GoSuda

Go 1.25 encoding/json/v2 működésbeli változásai és a v1 kompatibilitási réteg elemzése

By lemonmint views ...

A Go encoding/json csomagjának v2 verziója a v1 több hátrányának (következetlenség, meglepő viselkedés, teljesítményproblémák) orvoslására létrehozott új megvalósítás. Ez a goexperiment.jsonv2 build címke segítségével aktiválható kísérleti funkció.

A legfontosabb megállapítás, hogy ha a v2 aktív, a v1 a v2 megvalósítása fölött működő kompatibilitási rétegként viselkedik, amely emulálja a v1 működését. Ez a v2_v1.go fájl DefaultOptionsV1() függvényén keresztül valósul meg. Vagyis a v2 olyan opciókat biztosít, amelyekkel a v1 viselkedése tökéletesen reprodukálható, ugyanakkor szigorúbb és kiszámíthatóbb új alapértelmezett viselkedést kínál.

A v2 fő céljai a következők:

  1. A pontosság és a kiszámíthatóság javítása: Alapértelmezetten szigorúbb szabályokat alkalmaz (például megkülönbözteti a kis- és nagybetűket, tiltja az ismétlődő kulcsokat), ezzel csökkenti a váratlan viselkedést.
  2. Teljesítményjavítás: A feldolgozó és kódoló motort átalakították a hatékonyság növelése érdekében.
  3. Nagyobb rugalmasság és irányítási lehetőség: Részletes opciórendszert vezet be, amely lehetővé teszi a fejlesztők számára, hogy finoman szabályozzák a JSON-feldolgozás módját.

Főbb jelentésbeli és viselkedésbeli különbségek

A v2_test.go fájlra támaszkodva, pontokba szedve foglaltuk össze a v1 és a v2 viselkedése közötti különbségeket.

1. Mezőnevek illesztése (kis- és nagybetű megkülönböztetése)

  • v1 viselkedés: Amikor a JSON objektum tagjait Go struct mezőkbe demarshaláljuk, a kis- és nagybetűket figyelmen kívül hagyva (case-insensitive) illeszt. A "FirstName" és a "firstname" is a FirstName mezőhöz kerül.
  • v2 viselkedés: Alapértelmezetten megkülönbözteti a kis- és nagybetűket (case-sensitive), és csak a pontosan egyező mezőket állítja be.
  • A változtatás oka: A kis- és nagybetűk figyelmen kívül hagyása meglepő viselkedést okozhat, és a nem illeszkedő mezők kezelésekor teljesítménycsökkenést idézhet elő. A v2 a világosabb és kiszámíthatóbb viselkedést tette alapértelmezetté.
  • Kapcsolódó opciók: A v2-ben a json:"...,case:ignore" tagopcióval mezőnként kifejezetten engedélyezhető a kis- és nagybetűk figyelmen kívül hagyása, vagy globálisan alkalmazható a json.MatchCaseInsensitiveNames(true) opció.

2. Az omitempty tagopció jelentésének megváltozása

  • v1 viselkedés: A mezőt a Go-érték „üres állapota” alapján hagyja ki. Ez alatt a false, a 0, a nil mutató/interfész, valamint a nulla hosszúságú tömb/szelet/map/string értendő.
  • v2 viselkedés: A mezőt a kódolt JSON-érték „üres állapota” alapján hagyja ki. Vagyis akkor marad ki, ha null, "", {} vagy [] formában kódolódik.
  • A változtatás oka: A v1 definíciója a Go típusrendszeréhez kötött. A v2 a JSON típusrendszerét veszi alapul, ezáltal következetesebb viselkedést biztosít. Például a v1-ben a bool típus false értéke kimarad, míg a v2-ben a false nem üres JSON-érték, ezért nem marad ki. A v2 bevezeti az omitzero opciót, amellyel a v1 omitempty 0-ra vagy false-ra vonatkozó viselkedése helyettesíthető.
  • Kapcsolódó opciók: Ha a v1-gyel azonos viselkedést szeretnénk v2-ben, használjuk a json.OmitEmptyWithLegacyDefinition(true) opciót.

3. A string tagopció viselkedésének megváltozása

  • v1 viselkedés: Számok, logikai értékek és string típusú mezőkre alkalmazható. Az értéket újra kódolja egy JSON-stringbe (például int(42) -> "42"). Összetett típusok (szeletek, mapek stb.) belső értékeire nem alkalmazza rekurzívan.
  • v2 viselkedés: Kizárólag numerikus típusokra alkalmazható, és rekurzívan érvényesül. Vagyis a []int típusú szeletben lévő számok is mind JSON-stringként kerülnek kódolásra.
  • A változtatás oka: A string opció fő célja, hogy a 64 bites egészek pontosságvesztését elkerülje azáltal, hogy a számokat stringként ábrázolja. A v1 viselkedése korlátozott volt és következetlen. A v2 erre a fő felhasználásra összpontosít, és a viselkedést rekurzívan kiterjesztve hasznosabbá tette.
  • Kapcsolódó opciók: A json.StringifyWithLegacySemantics(true) opcióval utánozható a v1 viselkedése.

4. nil szeletek és mapek marshalálása

  • v1 viselkedés: A nil szeletek és a nil mapek null értékként marshalálódnak.
  • v2 viselkedés: Alapértelmezetten a nil szeletek [] (üres tömb), a nil mapek {} (üres objektum) formában marshalálódnak.
  • A változtatás oka: A nil a Go nyelv implementációs részlete, és nem célszerű ezt a nyelvtől független JSON-formátumban megjeleníteni. Az üres gyűjteményeket jelölő [] és {} általánosabb ábrázolások.
  • Kapcsolódó opciók: A v2-ben a json.FormatNilSliceAsNull(true) vagy a json.FormatNilMapAsNull(true) opcióval érhető el, hogy a v1-hez hasonlóan null értékként marshalálódjanak.

5. Tömbök demarshalálása

  • v1 viselkedés: Go tömbbe ([N]T) történő demarshaláláskor a JSON-tömb hossza eltérhet a Go tömb hosszától anélkül, hogy hibát okozna. Ha a JSON-tömb rövidebb, a fennmaradó helyeket nulla értékekkel tölti fel, ha hosszabb, a többletet eldobja.
  • v2 viselkedés: A JSON-tömb hosszának pontosan meg kell egyeznie a Go tömb hosszával. Ellenkező esetben hiba keletkezik.
  • A változtatás oka: Go-ban a fix méretű tömbök hossza gyakran fontos jelentéssel bír. A v1 viselkedése csendes adatvesztést okozhat. A v2 szigorúbb szabályokkal növelte a pontosságot.
  • Kapcsolódó opciók: A json.UnmarshalArrayFromAnyLength(true) opcióval utánozható a v1 viselkedése.

6. A time.Duration kezelése

  • v1 viselkedés: A time.Duration belsőleg int64-ként kezelődik, és nanoszekundumos JSON-számként kódolódik.
  • v2 viselkedés: A time.Duration.String() metódust használva, "1h2m3s" típusú formátumú JSON-stringként kódolódik.
  • A változtatás oka: A nyers nanoszekundumos számok nehezen olvashatók, a time.Duration szabványos stringes ábrázolása pedig hasznosabb.
  • Kapcsolódó opciók: A json:",format:nano" tagopcióval vagy a json.FormatTimeWithLegacySemantics(true) opcióval használható a v1 viselkedése.

7. Érvénytelen UTF-8 kezelése

  • v1 viselkedés: Marshalálás/demarshalálás során, ha egy stringben érvénytelen UTF-8 bájt található, a rendszer azt csendben a Unicode pótkarakterre (\uFFFD) cseréli.
  • v2 viselkedés: Alapértelmezetten hibát ad vissza, ha érvénytelen UTF-8 találkozik.
  • A változtatás oka: Az adatok csendes sérülésének megelőzése, valamint a szigorúbb JSON-szabvány (RFC 7493) követése.
  • Kapcsolódó opciók: A jsontext.AllowInvalidUTF8(true) opcióval utánozható a v1 viselkedése.

8. Ismétlődő objektumtag-nevek kezelése

  • v1 viselkedés: Megengedi, hogy egy JSON-objektumban azonos nevű tagok ismétlődjenek. Az utolsó megjelenő érték felülírja a korábbiakat.
  • v2 viselkedés: Alapértelmezetten hibát ad vissza, ha ismétlődő tagnév található.
  • A változtatás oka: Az RFC 8259 szabvány nem határozza meg az ismétlődő nevek viselkedését, így a megvalósítások eltérően működhetnek. Ez biztonsági sebezhetőség forrása lehet. A v2 kifejezetten elutasítja ezeket, ezzel növelve a pontosságot és a biztonságot.
  • Kapcsolódó opciók: A jsontext.AllowDuplicateNames(true) opcióval utánozható a v1 viselkedése.

Megvalósítási és architekturális különbségek

  • v1: Nagymértékben támaszkodik a decode.go fájlban lévő decodeState-re és a scanner.go fájlban kézzel írt állapotgépre (state machine). Ez egy monolitikus szerkezet, ahol a feldolgozási logika és a szemantikai elemzés szorosan összefonódik.
  • v2: Az architektúra moduláris:
    • encoding/json/jsontext: Alacsony szintű, nagy teljesítményű JSON-tokenizálót (Decoder) és kódolót (Encoder) biztosít. Ez a csomag kizárólag a JSON szintaktikai oldalára koncentrál.
    • encoding/json/v2: A jsontext-re építve kezeli a Go típusok és a JSON-értékek közötti szemantikai átalakítást.
    • Ez a szétválasztás elkülöníti a szintaktikai és a szemantikai elemzést, ami javítja a kód átláthatóságát és a teljesítményt.

A v2 új API-ja és funkciói

A v2 a json.Options rendszeren keresztül rendkívül rugalmas vezérlési lehetőségeket biztosít.

  • json.Options: A marshalálási/demarshalálási viselkedést módosító opciók halmaza.
  • json.JoinOptions(...): Több opciót egyesít egyetlen opcióba.
  • WithMarshalers / WithUnmarshalers: Hatékony funkció, amellyel egy adott típushoz a Marshaler/Unmarshaler interfész implementálása nélkül injektálható szerializációs/deszerializációs logika. Ez különösen hasznos külső csomagok típusainak kezelésekor.
  • Új opciók: Az RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull és más opciók olyan viselkedésvezérlést tesznek lehetővé, amely v1-ben nem volt elérhető.

Következtetés

Az encoding/json v2 a v1 tapasztalataira építve jelentősen javítja a pontosságot, a teljesítményt és a rugalmasságot, és modern megvalósítást nyújt. Az alapértelmezett viselkedés szigorúbbá vált, de a kifinomult Options rendszerrel a v1 összes viselkedése teljes mértékben támogatott, így a meglévő kóddal való kompatibilitás megmarad, és a v2 előnyei fokozatosan bevezethetők.

  • Új projekt esetén érdemes alapértelmezettként a v2-t használni.
  • Meglévő projektek használhatják változatlanul a jsonv1-et, vagy migrálhatnak jsonv2-re úgy, hogy a DefaultOptionsV1() segítségével fokozatosan vezetik be a v2 szigorúbb viselkedését.