Анализ на промените в поведението на Go 1.25 encoding/json/v2 и слоя за съвместимост с v1

Пакетът encoding/json v2 на Go е нова реализация, предназначена да отстрани няколко недостатъка на v1 (липса на последователност, неочаквано поведение, проблеми с производителността). Тя е експериментална функционалност, която се активира чрез build тага goexperiment.jsonv2.
Най-важното е, че когато v2 е активиран, v1 функционира като слой за съвместимост, който емулира поведението на v1 върху реализацията на v2. Това се осъществява чрез функцията DefaultOptionsV1() във файла v2_v1.go. С други думи, v2 предоставя опции, с които поведението на v1 може да се възпроизведе напълно, а едновременно с това въвежда по-строго и предвидимо ново поведение по подразбиране.
Основните цели на v2 са следните:
- Подобряване на коректността и предвидимостта: По подразбиране се прилагат по-строги правила (например чувствителност към регистъра и забрана на дублиращи се ключове), за да се намалi неочакваното поведение.
- Подобряване на производителността: Машината за парсване и кодиране е преproектирана с цел по-висока ефективност.
- Повече гъвкавост и контрол: Въведена е подробна система от опции, която позволява на разработчиците да контролират в детайли обработката на JSON.
Основни разлики в семантиката и поведението
Разликите между v1 и v2 са обобщени по точки, като се основават главно на файла v2_test.go.
1. Съпоставяне на имена на полета (чувствителност към регистъра)
- Поведение в v1: При unmarshal на член на JSON обект в поле на Go структура съпоставянето се извършва без значение на регистъра (case-insensitive). И
"FirstName", и"firstname"се съпоставят с полетоFirstName. - Поведение в v2: По подразбиране съпоставянето зачита регистъра (case-sensitive) и се свързват само полетата, които съвпадат точно.
- Причина за промяната: Съпоставянето без значение на регистъра може да доведе до неочаквано поведение и да понижи производителността при обработка на несъвпадащи полета. v2 приема по-ясно и предвидимо поведение като основно.
- Свързани опции: В v2 може да се използва таговата опция
json:"...,case:ignore", с която за отделни полета явно се включва съпоставяне без значение на регистъра, или глобалната опцияjson.MatchCaseInsensitiveNames(true).
2. Промяна в значението на таговата опция omitempty
- Поведение в v1: Полето се пропуска според "празното състояние" на Go стойността. "Празно състояние" означава
false,0,nilуказател/интерфейс или масив/слайс/map/низ с дължина 0. - Поведение в v2: Полето се пропуска според "празното състояние" на кодираната JSON стойност. Това означава, че се пропуска, когато стойността се кодира като
null,"",{}или[]. - Причина за промяната: Дефиницията в v1 зависи от системата от типове на Go. v2 се основава на системата от типове на JSON и по този начин осигурява по-последователно поведение. Например в v1 стойността
falseнаboolтип се пропуска, докато в v2falseе непразна JSON стойност и не се пропуска. В v2 е добавена опциятаomitzero, която може да замени поведението наomitemptyв v1 по отношение на0иfalse. - Свързани опции: За да се постигне поведение, идентично с v1, се използва опцията
json.OmitEmptyWithLegacyDefinition(true).
3. Промяна в поведението на таговата опция string
- Поведение в v1: Прилага се към полета от числов, булев и низов тип. Съответната стойност се кодира отново в JSON низ (например
int(42)->"42"). Не се прилага рекурсивно към стойности във вложени типове (слайсове, map и др.). - Поведение в v2: Прилага се само към числови типове и се прилага рекурсивно. Това означава, че числата във вътрешността на слайс като
[]intсъщо се кодират като JSON низове. - Причина за промяната: Основното предназначение на опцията
stringе да предотврати загуба на точност при 64-битови цели числа, като ги представя като низове. Поведението в v1 беше ограничено и непоследователно. v2 се съсредоточава върху това основно предназначение и разширява поведението рекурсивно, за да бъде по-полезно. - Свързани опции: С опцията
json.StringifyWithLegacySemantics(true)може да се имитира поведението на v1.
4. Маршалиране на nil слайсове и map-ове
- Поведение в v1:
nilслайсовете иnilmap-овете се маршалират катоnull. - Поведение в v2: По подразбиране
nilслайсовете се маршалират като[](празен масив), аnilmap-овете като{}(празен обект). - Причина за промяната:
nilе детайл от реализацията на езика Go и е нежелателно той да се разкрива в формат на JSON, който не зависи от езика. Представянията[]и{}за празни колекции са по-универсални. - Свързани опции: В v2 с опциите
json.FormatNilSliceAsNull(true)илиjson.FormatNilMapAsNull(true)може да се маршалира катоnull, както в v1.
5. Unmarshal на масиви
- Поведение в v1: При unmarshal към Go масив (
[N]T) дължината на JSON масива може да не съвпада с дължината на Go масива, без да се предизвиква грешка. Ако JSON масивът е по-къс, останалите елементи се запълват с нулеви стойности, а ако е по-дълъг, излишъкът се отхвърля. - Поведение в v2: Дължината на JSON масива трябва да съвпада точно с дължината на Go масива. В противен случай се връща грешка.
- Причина за промяната: В Go дължината на масивите с фиксиран размер често има съществено значение. Поведението в v1 може да доведе до тиха загуба на данни. v2 въвежда по-строго правило, за да повиши коректността.
- Свързани опции: С опцията
json.UnmarshalArrayFromAnyLength(true)може да се имитира поведението на v1.
6. Обработка на time.Duration
- Поведение в v1:
time.Durationсе третира вътрешно катоint64и се кодира като JSON число в наносекунди. - Поведение в v2: Използва се методът
time.Duration.String(), така че стойността се кодира като JSON низ с формат като"1h2m3s". - Причина за промяната: Числата в наносекунди са трудно четими, а стандартното низово представяне на
time.Durationе по-полезно. - Свързани опции: Поведението на v1 може да се използва с таговата опция
json:",format:nano"или с опциятаjson.FormatTimeWithLegacySemantics(true).
7. Обработка на невалиден UTF-8
- Поведение в v1: При маршалиране и unmarshal, ако даден низ съдържа невалидни UTF-8 байтове, те се заменят безшумно със символа за заместване на Unicode (
\uFFFD). - Поведение в v2: По подразбиране при среща с невалиден UTF-8 се връща грешка.
- Причина за промяната: Целта е да се предотврати безшумно повреждане на данните и да се спазва по-строгият стандарт за JSON (RFC 7493).
- Свързани опции: С опцията
jsontext.AllowInvalidUTF8(true)може да се имитира поведението на v1.
8. Обработка на дублиращи се имена на членове в обект
- Поведение в v1: В JSON обект се допуска наличието на членове с едно и също име. Използва се последната поява, която презаписва предишните.
- Поведение в v2: По подразбиране при наличие на дублиращо се име на член се връща грешка.
- Причина за промяната: Стандартът RFC 8259 не дефинира поведението при дублиращи се имена, поради което различните реализации могат да се държат по различен начин. Това може да породи уязвимости в сигурността. v2 изрично отхвърля такива случаи, за да повиши коректността и сигурността.
- Свързани опции: С опцията
jsontext.AllowDuplicateNames(true)може да се имитира поведението на v1.
Разлики в реализацията и архитектурата
- v1: Силно зависи от
decodeStateвdecode.goи от ръчно написаната машина на състоянията (state machine) вscanner.go. Това е монолитна структура, в която логиката за парсване и семантичният анализ са силно свързани. - v2: Архитектурата е по-модулна.
encoding/json/jsontext: Предоставя нискониво и високопроизводително JSON токенизатор (Decoder) и кодер (Encoder). Този пакет се фокусира единствено върху синтактичните аспекти на JSON.encoding/json/v2: Въз основа наjsontextизвършва семантичното преобразуване между Go типове и JSON стойности.- Това разделение отделя синтактичния анализ от семантичния, което подобрява яснотата на кода и производителността.
Нови API и функционалности в v2
v2 предоставя много гъвкав контрол чрез системата json.Options.
json.Options: Набор от опции, които променят поведението при маршалиране и unmarshal.json.JoinOptions(...): Обединява няколко опции в една.WithMarshalers/WithUnmarshalers: Мощна функционалност, която позволява да се инжектира логика за сериализация и десериализация за определени типове, без те да имплементират интерфейситеMarshaler/Unmarshaler. Особено полезна е при работа с типове от външни пакети.- Нови опции:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNullи други, които позволяват контрол на поведения, невъзможни в v1.
Заключение
encoding/json v2 е съвременна реализация, която на базата на опита с v1 значително подобрява коректността, производителността и гъвкавостта. Поведението по подразбиране стана по-строго, но чрез разширената система от Options всички поведения на v1 могат да се поддържат напълно, така че съвместимостта с наличния код се запазва и предимствата на v2 могат да се въвеждат постепенно.
- За нови проекти препоръчително е v2 да се използва по подразбиране.
- Съществуващите проекти могат или да продължат да използват
jsonv1без промени, или да мигрират къмjsonv2, като постепенно въвеждат по-строгото поведение на v2 чрезDefaultOptionsV1().