GoSuda

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

By lemonmint views ...

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í:

  1. 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í.
  2. Zlepšení výkonu: Enginy pro parsování a kódování byly přepracovány, aby byla zvýšena efektivita.
  3. 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 pole FirstName.
  • 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žnost json.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, nil ukazatel č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 false typu bool vynechá, zatímco v v2 se nevynechá, protože false je v JSONu neprázdná hodnota. v v2 byla přidána možnost omitzero, která nahrazuje chování v1, kdy se omitempty uplatňovalo na 0 nebo false.
  • 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 []int jsou tedy také zakódována jako JSON řetězce.
  • Důvod změny: Hlavním účelem volby string je 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: nil slice a nil mapa se marshalují jako null.
  • Chování v2: Výchozím nastavením se nil slice marshaluje jako [] (prázdné pole) a nil mapa jako {} (prázdný objekt).
  • Důvod změny: nil je 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) nebo json.FormatNilMapAsNull(true) dosáhnout marshalingu jako null, 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.Duration je interně považován za int64 a 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í metody time.Duration.String().
  • Důvod změny: Číslo v nanosekundách je hůře čitelné a standardní řetězcová reprezentace time.Duration je užitečnější.
  • Související možnosti: Chování v1 lze použít pomocí tagu json:",format:nano" nebo možnosti json.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 decodeState v souboru decode.go a na ručně napsaném stavovém automatu (state machine) v souboru scanner.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ě jsontext zajišť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), FormatNilSliceAsNull a 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 na jsonv2 a postupně zavádět přísnější chování v2 pomocí DefaultOptionsV1().