Как стать автором
Обновить

Комментарии 30

Хороший пример для тех, кто еще не пробовал custom сериализацию.
Один момент смутил только, при вызове
if data[0] == 34 {
мы не делаем проверку на длину слайса. Но я допускаю, что эта проверка опущена для сокращения статьи)
Кажется, это дефект (issue) json-библиотеки. Там должен быть не []byte, а JsonValue (интерфейс?). И лежать в нем должен один из JsonNull, JsonString, JsonNumber, JsonBool, JsonObject, JsonArray.

Если у нас есть слайс байтов, значит сам json уже распаршен (как иначе могут быть найдены границы слайса?). Значит конструкции вида {"id":1, "price":} досюда дойти не могут — они будут отброшены раньше. Но это неочевидно (а может даже ошибочно — я не смотрел исходники) из-за интерфейса библиотеки.

Может, они там регуляркой скобки просто ищут, хотя, конечно, замечено правильно.

Регуляркой нельзя. Регулярка не отловит вложенность произвольного уровня. И «по-простому» нельзя — в строковом литерале может быть закрывающая скобка, ее нужно пропустить. Ну а если учитывать экранирование символов — так это уже полный парсинг и получается)

На самом деле, отловить произвольную вложенность регуляркой можно. Другое дело, что Golang это не поддерживает.

Вот и правильно. А то понапишут регэкспов с рекурсией и дьявола вызывают
Кажется, это дефект (issue) json-библиотеки. Там должен быть не []byte, а JsonValue (интерфейс?). И лежать в нем должен один из JsonNull, JsonString, JsonNumber, JsonBool, JsonObject, JsonArray.
Возвращать интерфейсы это лишние аллокации и оверхед, с которыми и так проблемы есть даже в текущей реализации, которую заменяют на что-то более быстрое. Возврат слайса в любом случае лучше, но могли еще вернуть тип значения текущего, но на практике ниразу это не было нужно. Если идет какая-то кастомная сериализация, то никогда даже внутрь этого слайса смотреть не надо было. Автор зачем-то полез внутрь, хотя достаточно было использовать interface{}.
Вот есть у нас джсон `{«internal1»:{«internal2»:{}}}`. Как он распарсится? Допустим, парсим мы не в interface{} а в конкретный тип. Окей, анмаршал весь слайс как конкретный объект. Прочитали токен «internal1», смотрим — сейчас должен быть объект типа Internal1. Нужно подготовить слайс для него, то есть понять где он заканчивается. Для этого нужно пройтись и полностью распарсить {«internal2»:{}} — чтобы понять, что это наша закрывающая скобка, что здесь слайс заканчивается. В том числе определить что полю «internal2» соответствует слайс {} — иначе как мы поймем что поле «internal2» закончилось и закрывающая скобка соответствует концу нашего internal1.

Окей, определили слайс для internal1, вызвали анмаршал для этого слайса. Теперь анмаршал будет снова парсить содержимое слайса {«internal2»:{}}, в том числе определять что полю «internal2» соответствует слайс {}.

Слайс — это недостаточное количество информации. Нужно либо передавать для каста к пользовательскому типу «размеченный слайс», decodeState, узел дерева разбора; либо парсинг будет неэффективным, одни и те же слайсы будут парситься по нескольку раз. Возможно, из-за второго и требуется заменять на «что-то более быстрое»

Если аллокации неэффективны — нужно искать способы повышать эффективность (выделять пачками-массивами?)
Если internal1 и internal2 оба реализуют метод UnmarshalJSON, то библиотека сама вызовет его только у internal1. То, что у него внутри вложенные типы со своим UnmarshalJSON, она пропустит. Как только встречается открывающая скобка объекта и библиотека видит, что для этого объекта есть кастомный анмаршалинг, она сразу бежит до конца этого объекта как бы вложенный он ни был. Ежели методы не реализованы, то библиотека использует рефлексию и парсит последовательно поля. Поэтому парсинг в библиотеке полностью потоковый и, собственно, есть json.Decoder, который с потоками и работает.

Слайс — это недостаточное количество информации

Для вызывающей стороны вполне достаточно. Задача этой библиотеки маршалить байты в структуры, а не предоставлять структуру JSON вызывающей стороне. Все эти типы данных JSON, токенизация, дерево разбора — все это хранится внутри и наружу не торчит.

Если аллокации неэффективны — нужно искать способы повышать эффективность (выделять пачками-массивами?)

Подобные примитивные оптимизации уже сделаны. Единственный реальный способ ускорения это генерация кода как в easyjson, либо отсутствие сериализации как fastjson, где парсер всего лишь находит границы полей и значений, а сериализация происходит только в момент обращения к конкретному полю по ключу методами вроде GetInt, GetBool. Даже строки не аллоцируются, а возвращается слайс из JSON строки.
она сразу бежит до конца этого объекта как бы вложенный он ни был


Бежит, проверяя валидность джсона внутри. Фактически, проводит работу по парсингу внутреннего объекта — без этого невозможно найти конец объекта.

библиотека использует рефлексию и парсит последовательно поля


Допустим у нас нет кастомного анмаршалинга. Как будет работать библиотека? Я предполагаю что так: «О, поле internal1 типа Internal1. Для типа нет кастомного анмаршалинга, что ж, найду конец поля internal1 и вызову рекурсивно анмаршалинг полученного слайса в тип Internal1». И по слайсу будет сделано два прохода — сначала с целью определить конец Internal1, потом чтобы собственно распарсить. Или конец поля ищется только для полей с кастомным анмаршаллингом?

Для вызывающей стороны вполне достаточно

Тут еще надо определить что вы имеете в виду под вызывающей стороной. Потребитель который вызывает условный JsonStringToObject должен предоставить только слайс/строку, да. А для восстановления объекта из кастомного джсон-представления библиотеке лучше бы использовать метод JsonNodeToObject. Нет кастомного анмаршалинга — вызывай JsonStringToObject и не парься. Есть — определи JsonNodeToObject и вызывай JsonStringToObject.

Вообще, в UnmarshalJSON всегда передается слайс, гарантированно являющийся JsonNode. По нему парсер уже прошелся и определил что он валидный (иначе бы парсер не нашел конец объекта). Тем не менее в сигнатуре это никак не отражено и единственное что с этим слайсом можно сделать — это вызвать json.Unmarshal(data, &raw). Который снова будет проверять, что в слайсе валидный джсон. На слайсах невозможно построить эффективный с точки зрения процессорного времени парсинг, по крайней мере кастомный
Или конец поля ищется только для полей с кастомным анмаршаллингом?

Естественно всегда ищется. Как еще сериализовать объект, если начала и конца не знаешь. Вся разница в том, что без кастомного он идет сам рекурсивно по полям. С кастомным ему главное найти конец объекта, быстро пройдясь по всем токенам до конца.

На слайсах невозможно построить эффективный с точки зрения процессорного времени парсинг, по крайней мере кастомный

Дело здесь не в слайсах, а в повторной валидации, если вызывать Unmarshal внутри UnmarshalJSON. И касаться это будет только объектов с кастомной сериализацией, что редкость. Значение полей все равно валидировать надо тому, кто делает сериализацию. encoding.Json максимум проверяет, что символы корректные для JSON в целом. С эффективностью тут все ок. В любом случае, это несущественная проблема в сравнении с аллокациями и если прямо надо быстро, то есть решения, которые действительно быстрые.

Слайсы это в целом правильный подход, т.к. они указывают на изначальный буфер. В любой высокопроизводительной библиотеке будет делаться так же. Даже если она будет возвращать Node, то внутри у него будет тот же слайс. И все это по максимуму будет с аллокациями на стеке.

Вообще, почитайте лучше сами исходники. Код там довольно простой и понятный.
На слайсах невозможно построить эффективный с точки зрения процессорного времени парсинг, по крайней мере кастомный

Уточню: на сырых слайсах.

Я не против слайсов в целом и не предлагаю аллоцировать новые буферы на каждый чих. Я считаю дефектом что в UnmarshalJSON передается сырой слайс (byte[]), а не завернутый в Node. Да, Node тоже надо аллоцировать, но это-то должно быть копейками.

Вызывать Unmarshal внутри UnmarshalJSON — кажется очень естественно, а текущие интерфейсы делают это неэффективным. Почитал исходники, если есть много вложенных объектов с кастомными анмаршалингами — то асимптотика становится квадратичной от уровня вложенности за счет повторных валидаций.

Спасибо, когда-то приходилось додумываться до этого самому, теперь людям будет проще находить решение такой проблемы.


Более того, можно даже написать


if data[0] == '"' {

Нетипизированная константа (руна '"') спокойно преобразовывается компилятором в byte, поскольку значение 34 все еще в диапазоне 0..255. А код становится немного понятнее для человека

Не обязательно было делать сложную структуру


type CustomBool struct {
    Bool bool
}

это всё можно заменить на


type CustomBool bool

func (cb CustomBool) UnmarshalJSON(data []byte) error {
    switch string(data) {
    case `"true"`, `true`, `"1"`, `1`:
        cb = true
        return nil
    case `"false"`, `false`, `"0"`, `0`, `""`:
        cb = false
        return nil
    default:
        return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
    }
}

и метод func (cb CustomBool) MarshalJSON() ([]byte, error) будет не нужен.
Пример: https://play.golang.com/p/QcxGmAb8Jlt

CustomBool не надо ли по ссылке в reciver принимать, а то результат присвоения будет отброшен. Я думаю текущий код даст ошибку или предупреждение о неиспользуемом значении.

Ваш код печатает false, передаётся true.

Нельзя писать код без тестов после тяжелой недели.
Разумеется должен быть указатель


func (cb *CustomBool) UnmarshalJSON(data []byte) error {
    switch string(data) {
    case `"true"`, `true`, `"1"`, `1`:
        *cb = true
        return nil
    case `"false"`, `false`, `"0"`, `0`, `""`:
        *cb = false
        return nil
    default:
        return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
    }
}

https://play.golang.com/p/Sa_Dobxd3sh

Тут, как мне кажется, или быть строгими и как в спецификации — “только true/false”, или уже всё, что не 0, как истинное значение трактовать.

Ответ сразу и ARechitsky: это не мой код, я взял листинг из статьи и изменил сложную структуру на тип, оставив всю логику как была.


Я бы вообще предпочел так не делать, но уж если есть такая необходимость то или предобрабатывать данные исправляя подобные логические ошибки, или в одном конкретном поле одного типа сделать понимание конкретных строк и boolean из json

И так я тоже делать бы не стал. 01 например — это 1 с точки зрения джсона. А с точки зрения вашего кода — ошибка. Тоже распарсить в interface{}, посмотреть тип и значение и тогда решать.
И вообще, не нужно руками лезть в сырой джсон.
Сначала unmarshal в interface{}, а потом смотреть что там — строка или не строка
play.golang.com/p/Fl6yQHh-Hyj
Привет! Добавил в твой код
значение в json 3 или 3.0 — он выдал ошибку

jsonString := `[{"id":1,"price":"2.15"}, {"id":2,"price":3.7},{"id":2,"price":3.0}]]`

	targets := []Target{}

	_ = json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))


Можешь сам проверить в песочнице. Я без критики, но если добавить в код еще и проверку на целое число или целое число сконкатенированное с .0, то работало бы.
Просто к тому пишу, что это вопрос реализации проверки значения до ообработки.

Лишняя закрывающая квадратная скобка массива. Все работает play.golang.com/p/0qJBtd4HaPi
А лучше даже так, с обработкой ошибки парсинга play.golang.com/p/tPYQZFGLajT
Спасибо! буду сам пользоваться!
За
err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)

надо отдельно бить линейкой по рукам. Кто вам сказал что json.Unmarshal для float64 ведет себя как обычный парсинг числа из строки? Изучили ли вы полностью стандарт json, чтобы утверждать что да? Что будет с научной записью, например — если она вдруг появится? (Тут я сам наверняка сказать не могу). Нужно использовать strconv.ParseFloat().

А как поведет себя парсинг части json-строки как числа? Вот тут могу привести пример — строка "\u0032\u002e\u0031\u0035" эквивалентна строке «2.15». Но на первой ваш код сломается, а по смыслу должен работать.
Я вижу много критики в адрес автора, в некоторых случаях довольно здравой, однако хотел бы заметить, что подход им описанный интересный и правильный.

Действительно функция Unmarshal в стандартной библиотеке берет на вход слайс байтов и интерфейс — так что было бы круто исправить это.

//
func Unmarshal(data []byte, v interface{}) error {
	// Check for well-formedness.
	// Avoids filling out half a data structure
	// before discovering a JSON syntax error.
	var d decodeState
	err := checkValid(data, &d.scan)
	if err != nil {
		return err
	}

	d.init(data)
	return d.unmarshal(v)
}


НО! у меня была такая же проблема с обработкой json'а и я использовал interface'ы и type assertion, что совсем не круто.

Опять же критика отлова кавычек строки в json'е — тоже верная, но лишь дополняет логику условий при определении типа значения перед его обработкой. Можно и strconv, можно и через rune вообще проверить — это вопрос реализации.

Так что, лично я, очень благодарен YuriiPlohov за эту статью и отличный подход в обработке json'а в Go.

Нет, надо не «проверить», надо определить правила преобразования. Брать слайс из сырой джсон-строки между кавычек и интерпретировать его как джсон-число — однозначно порочный подход. Гораздо лучше распарсить джсон как есть, посмотреть что распарсилось (какие типы) и исходя из этого сделать явное преобразование в нужный тип.

Боюсь, исправить Unmarshal так просто не выйдет, это значит что надо разделять парсинг на две части, разбор из строки в дерево джсон-узлов, потом конвертацию узла дерева в заданный тип. Первое всегда общее, второе — можно доопределять для пользовательских типов.
Разве не проще перед десериализацией обработать входные данные?
$jsondata =~s/\"([\d\.]+)\"/$1/g;
(для числовых данных, perl)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории