Комментарии 30
Один момент смутил только, при вызове
if data[0] == 34 {
мы не делаем проверку на длину слайса. Но я допускаю, что эта проверка опущена для сокращения статьи)Если у нас есть слайс байтов, значит сам json уже распаршен (как иначе могут быть найдены границы слайса?). Значит конструкции вида
{"id":1, "price":}
досюда дойти не могут — они будут отброшены раньше. Но это неочевидно (а может даже ошибочно — я не смотрел исходники) из-за интерфейса библиотеки.Может, они там регуляркой скобки просто ищут, хотя, конечно, замечено правильно.
На самом деле, отловить произвольную вложенность регуляркой можно. Другое дело, что Golang это не поддерживает.
Кажется, это дефект (issue) json-библиотеки. Там должен быть не []byte, а JsonValue (интерфейс?). И лежать в нем должен один из JsonNull, JsonString, JsonNumber, JsonBool, JsonObject, JsonArray.Возвращать интерфейсы это лишние аллокации и оверхед, с которыми и так проблемы есть даже в текущей реализации, которую заменяют на что-то более быстрое. Возврат слайса в любом случае лучше, но могли еще вернуть тип значения текущего, но на практике ниразу это не было нужно. Если идет какая-то кастомная сериализация, то никогда даже внутрь этого слайса смотреть не надо было. Автор зачем-то полез внутрь, хотя достаточно было использовать interface{}.
Окей, определили слайс для internal1, вызвали анмаршал для этого слайса. Теперь анмаршал будет снова парсить содержимое слайса {«internal2»:{}}, в том числе определять что полю «internal2» соответствует слайс {}.
Слайс — это недостаточное количество информации. Нужно либо передавать для каста к пользовательскому типу «размеченный слайс», decodeState, узел дерева разбора; либо парсинг будет неэффективным, одни и те же слайсы будут парситься по нескольку раз. Возможно, из-за второго и требуется заменять на «что-то более быстрое»
Если аллокации неэффективны — нужно искать способы повышать эффективность (выделять пачками-массивами?)
Слайс — это недостаточное количество информации
Для вызывающей стороны вполне достаточно. Задача этой библиотеки маршалить байты в структуры, а не предоставлять структуру 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")
}
}
Тут, как мне кажется, или быть строгими и как в спецификации — “только true/false”, или уже всё, что не 0, как истинное значение трактовать.
Тут одни натрактовали на несколько килотонн угля:
https://habr.com/ru/company/digital-ecosystems/blog/501536/
Ответ сразу и ARechitsky: это не мой код, я взял листинг из статьи и изменил сложную структуру на тип, оставив всю логику как была.
Я бы вообще предпочел так не делать, но уж если есть такая необходимость то или предобрабатывать данные исправляя подобные логические ошибки, или в одном конкретном поле одного типа сделать понимание конкретных строк и boolean из json
Сначала 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, то работало бы.
Просто к тому пишу, что это вопрос реализации проверки значения до ообработки.
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)
Go: десериализация JSON с неправильной типизацией, или как обходить ошибки разработчиков API