Analýza změn chování encoding/json/v2 v Go 1.25 a vrstvy kompatibility s v1

Balíček encoding/json v2 v Go je novou implementací, jejímž cílem je odstranit několik nedostatků stávající verze v1 (nedostatečnou konzistenci, překvapivé chování a problémy s výkonem). Jde o experimentální funkci, která se aktivuje pomocí build tagu goexperiment.jsonv2.
Nejdůležitější je, že když je v2 aktivní, v1 funguje jako kompatibilní vrstva, která na implementaci v2 emuluje chování v1. Děje se tak prostřednictvím funkce DefaultOptionsV1() v souboru v2_v1.go. To znamená, že v2 nabízí možnosti, které dokážou chování v1 zcela reprodukovat, a zároveň zavádí přísnější a předvídatelnější nové výchozí chování.
Hlavní cíle v2 jsou následující:
- Zvýšení správnosti a předvídatelnosti: Výchozím nastavením jsou uplatňována přísnější pravidla (např. rozlišování velikosti písmen, zákaz duplicitních klíčů), čímž se omezuje neočekávané chování.
- Zlepšení výkonu: Enginy pro parsování a kódování byly přepracovány, aby byla zvýšena efektivita.
- Větší flexibilita a kontrola: Byl zaveden podrobný systém možností, který vývojářům umožňuje jemně řídit způsob zpracování JSON.
Hlavní rozdíly ve významu a chování
Rozdíly mezi v1 a v2 jsou níže uspořádány podle položek se zaměřením na soubor v2_test.go.
1. Porovnávání názvů polí (rozlišování velikosti písmen)
- Chování v1: Při unmarshalingu členů JSON objektu do polí struktury v Go se velikost písmen ignoruje (case-insensitive). Hodnoty
"FirstName"i"firstname"se namapují na poleFirstName. - Chování v2: Výchozím nastavením se rozlišuje velikost písmen (case-sensitive) a namapují se pouze pole, jejichž název přesně odpovídá.
- Důvod změny: Porovnávání bez ohledu na velikost písmen může způsobovat neočekávané chování a při zpracování neodpovídajících polí snižuje výkon. v2 proto jako výchozí přijala jasnější a předvídatelnější chování.
- Související možnosti: V v2 lze tag
json:"...,case:ignore"použít k explicitnímu zapnutí ignorování velikosti písmen pro jednotlivá pole, nebo lze globálně aplikovat možnostjson.MatchCaseInsensitiveNames(true).
2. Změna významu volby omitempty
- Chování v1: Pole se vynechá na základě „prázdného stavu“ hodnoty v Go. „Prázdným stavem“ se zde rozumí
false,0,nilukazatel či rozhraní a pole, řetězec, slice nebo mapa s délkou 0. - Chování v2: Pole se vynechá na základě „prázdného stavu“ zakódované JSON hodnoty. Vynechá se tedy tehdy, je-li zakódována jako
null,"",{}nebo[]. - Důvod změny: Definice v1 závisí na typovém systému Go. v2 vychází z typového systému JSON, a poskytuje tak konzistentnější chování. Například v v1 se hodnota
falsetypuboolvynechá, zatímco v v2 se nevynechá, protožefalseje v JSONu neprázdná hodnota. v v2 byla přidána možnostomitzero, která nahrazuje chování v1, kdy seomitemptyuplatňovalo na0nebofalse. - Související možnosti: Pokud chcete v v2 chování shodné s v1, použijte možnost
json.OmitEmptyWithLegacyDefinition(true).
3. Změna chování volby string
- Chování v1: Použije se u polí číselného, logického a řetězcového typu. Příslušná hodnota se znovu zakóduje uvnitř JSON řetězce (např.
int(42)->"42"). Na hodnoty uvnitř složených typů (slice, mapa apod.) se nepoužije rekurzivně. - Chování v2: Použije se pouze u číselných typů a aplikuje se rekurzivně. Čísla uvnitř slicu typu
[]intjsou tedy také zakódována jako JSON řetězce. - Důvod změny: Hlavním účelem volby
stringje zabránit ztrátě přesnosti 64bitových celých čísel tím, že se čísla vyjádří jako řetězce. Chování v1 bylo omezené a nekonzistentní. v2 se soustředí na tento klíčový účel a rozšiřuje chování rekurzivně, čímž je užitečnější. - Související možnosti: Chování v1 lze napodobit pomocí možnosti
json.StringifyWithLegacySemantics(true).
4. Marshaling hodnot nil pro slice a mapy
- Chování v1:
nilslice anilmapa se marshalují jakonull. - Chování v2: Výchozím nastavením se
nilslice marshaluje jako[](prázdné pole) anilmapa jako{}(prázdný objekt). - Důvod změny:
nilje implementační detail jazyka Go a není vhodné jej vystavovat ve formátu JSON, který nezávisí na jazyce. Pro vyjádření prázdné kolekce jsou běžnější[]a{}. - Související možnosti: V v2 lze pomocí možností
json.FormatNilSliceAsNull(true)nebojson.FormatNilMapAsNull(true)dosáhnout marshalingu jakonull, jako v v1.
5. Unmarshaling polí
- Chování v1: Při unmarshalingu do pole Go (
[N]T) nevznikne chyba, i když se délka JSON pole liší od délky pole Go. Pokud je JSON pole kratší, zbylé prvky se vyplní nulovými hodnotami, a pokud je delší, přebytek se zahodí. - Chování v2: Délka JSON pole se musí přesně shodovat s délkou pole v Go. V opačném případě dojde k chybě.
- Důvod změny: V Go má délka pole s pevnou velikostí často zásadní význam. Chování v1 může tiše způsobit ztrátu dat. v2 přísnějším pravidlem zvyšuje správnost.
- Související možnosti: Chování v1 lze napodobit pomocí možnosti
json.UnmarshalArrayFromAnyLength(true).
6. Zpracování time.Duration
- Chování v1:
time.Durationje interně považován zaint64a kóduje se jako číslo v JSON v nanosekundách. - Chování v2: Kóduje se jako JSON řetězec ve formátu jako
"1h2m3s"pomocí metodytime.Duration.String(). - Důvod změny: Číslo v nanosekundách je hůře čitelné a standardní řetězcová reprezentace
time.Durationje užitečnější. - Související možnosti: Chování v1 lze použít pomocí tagu
json:",format:nano"nebo možnostijson.FormatTimeWithLegacySemantics(true).
7. Zpracování neplatného UTF-8
- Chování v1: Při marshalingu/unmarshalingu se neplatné bajty UTF-8 v řetězcích tiše nahradí Unicode náhradním znakem (
\uFFFD). - Chování v2: Výchozím nastavením se při narazení na neplatné UTF-8 vrátí chyba.
- Důvod změny: Cílem je zabránit tichému poškození dat a dodržet přísnější standard JSON (RFC 7493).
- Související možnosti: Chování v1 lze napodobit pomocí možnosti
jsontext.AllowInvalidUTF8(true).
8. Zpracování duplicitních názvů členů objektu
- Chování v1: V JSON objektu je povoleno, aby se člen se stejným názvem vyskytoval vícekrát. Použije se hodnota, která se objeví jako poslední.
- Chování v2: Výchozím nastavením se při existenci duplicitního názvu člena vrátí chyba.
- Důvod změny: Standard RFC 8259 nedefinuje chování u duplicitních názvů, takže se implementace mohou lišit. Může to být příčinou bezpečnostních zranitelností. v2 tyto duplicity explicitně odmítá, čímž zvyšuje správnost a bezpečnost.
- Související možnosti: Chování v1 lze napodobit pomocí možnosti
jsontext.AllowDuplicateNames(true).
Rozdíly v implementaci a architektuře
- v1: Silně závisí na
decodeStatev souborudecode.goa na ručně napsaném stavovém automatu (state machine) v souboruscanner.go. Jde o monolitickou strukturu, v níž jsou parsovací logika a sémantická analýza úzce provázány. - v2: Architektura je více modulární.
encoding/json/jsontext: Poskytuje nízkoúrovňový vysoce výkonný tokenizér JSON (Decoder) a enkodér (Encoder). Tento balíček se zaměřuje výhradně na syntaktickou stránku JSON.encoding/json/v2: Na základějsontextzajišťuje sémantickou transformaci mezi typy Go a hodnotami JSON.- Toto oddělení odděluje syntaktickou analýzu od sémantické analýzy, což zvyšuje přehlednost kódu a výkon.
Nová API a funkce v2
v2 nabízí velmi flexibilní řízení prostřednictvím systému json.Options.
json.Options: Množina možností, které mění chování při marshalingu/unmarshalingu.json.JoinOptions(...): Spojí více možností do jedné.WithMarshalers/WithUnmarshalers: Výkonná funkce, která umožňuje vložit logiku serializace/deserializace pro konkrétní typ, aniž by typ musel implementovat rozhraníMarshaler/Unmarshaler. Je užitečná zejména při práci s typy z externích balíčků.- Nové možnosti:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNulla další umožňují řídit chování, které ve v1 nebylo možné.
Závěr
encoding/json v2 je moderní implementací, která na základě zkušeností s v1 výrazně zlepšuje správnost, výkon a flexibilitu. Výchozí chování je přísnější, ale díky propracovanému systému Options lze plně podpořit všechna chování v1, takže lze zachovat kompatibilitu se stávajícím kódem a postupně přijímat výhody v2.
- Pro nové projekty se doporučuje používat v2 jako výchozí volbu.
- Stávající projekty mohou buď nadále používat
jsonv1, nebo migrovat najsonv2a postupně zavádět přísnější chování v2 pomocíDefaultOptionsV1().