GoSuda

Analyse van de gedragswijzigingen in encoding/json/v2 van Go 1.25 en de compatibiliteitslaag met v1

By lemonmint views ...

Het encoding/json-pakket v2 in Go is een nieuwe implementatie die is ontworpen om verschillende tekortkomingen van de bestaande v1 te verbeteren (gebrek aan consistentie, verrassend gedrag, prestatieproblemen). Het is een experimentele functie die wordt geactiveerd via de build-tag goexperiment.jsonv2.

Het belangrijkste punt is dat wanneer v2 is geactiveerd, v1 fungeert als een compatibiliteitslaag die het gedrag van v1 emuleert bovenop de v2-implementatie. Dit wordt gerealiseerd via de functie DefaultOptionsV1() in het bestand v2_v1.go. Met andere woorden, v2 biedt opties waarmee het gedrag van v1 volledig kan worden gereproduceerd, en introduceert tegelijkertijd nieuw standaardgedrag dat strenger en voorspelbaarder is.

De belangrijkste doelen van v2 zijn als volgt.

  1. Verbetering van correctheid en voorspelbaarheid: Door standaard strengere regels toe te passen (bijvoorbeeld hoofdlettergevoeligheid en verboden dubbele sleutels) worden onverwachte gedragingen beperkt.
  2. Prestatieverbetering: De parsing- en coderingsengine is opnieuw ontworpen om de efficiëntie te verhogen.
  3. Meer flexibiliteit en controle: Een gedetailleerd optiesysteem stelt ontwikkelaars in staat de verwerking van JSON fijnmazig te controleren.

Belangrijkste verschillen in semantiek en gedrag

Met het bestand v2_test.go als uitgangspunt zijn de gedragsverschillen tussen v1 en v2 per onderwerp samengevat.

1. Matching van veldnamen (hoofdlettergevoeligheid)

  • Gedrag van v1: Bij het unmarshalen van JSON-objectleden naar Go-structvelden wordt hoofdletterongevoelig (case-insensitive) gematcht. Zowel "FirstName" als "firstname" worden aan het veld FirstName gekoppeld.
  • Gedrag van v2: Standaard wordt hoofdlettergevoelig (case-sensitive) gematcht, waarbij alleen velden met een exact overeenkomende naam worden gekoppeld.
  • Reden voor de wijziging: Hoofdletterongevoelige matching kan een bron van onverwacht gedrag zijn en veroorzaakt prestatieverlies bij het verwerken van niet-overeenkomende velden. v2 hanteert als standaard een duidelijker en voorspelbaarder gedrag.
  • Gerelateerde opties: In v2 kan hoofdletterongevoelige matching expliciet per veld worden ingeschakeld met de tagoptie json:"...,case:ignore", of globaal worden toegepast met de optie json.MatchCaseInsensitiveNames(true).

2. Wijziging in de betekenis van de tagoptie omitempty

  • Gedrag van v1: Velden worden weggelaten op basis van de "lege staat" (empty state) van de Go-waarde. Met "lege staat" wordt bedoeld false, 0, nil-pointers/interfaces en arrays/slices/maps/strings met lengte nul.
  • Gedrag van v2: Velden worden weggelaten op basis van de "lege staat" van de gecodeerde JSON-waarde. Dat wil zeggen dat velden worden weggelaten wanneer ze worden gecodeerd als null, "", {} of [].
  • Reden voor de wijziging: De definitie van v1 is afhankelijk van het typesysteem van Go. v2 baseert zich op het JSON-typesysteem en biedt daardoor consistenter gedrag. In v1 wordt bijvoorbeeld de waarde false van een bool-veld weggelaten, terwijl in v2 false een niet-lege JSON-waarde is en dus niet wordt weggelaten. In v2 is de optie omitzero toegevoegd, waarmee het gedrag van omitempty van v1 voor 0 of false kan worden vervangen.
  • Gerelateerde optie: Om in v2 exact het gedrag van v1 te verkrijgen, wordt de optie json.OmitEmptyWithLegacyDefinition(true) gebruikt.

3. Wijziging in het gedrag van de tagoptie string

  • Gedrag van v1: De optie is van toepassing op velden van numerieke, booleaanse en stringtypen. De waarde wordt opnieuw binnen een JSON-string gecodeerd (bijvoorbeeld int(42) -> "42"). Op waarden binnen samengestelde typen (slices, maps, enzovoort) wordt de optie niet recursief toegepast.
  • Gedrag van v2: De optie is uitsluitend van toepassing op numerieke typen en wordt recursief toegepast. Dat betekent dat ook de getallen binnen een slice zoals []int allemaal als JSON-string worden gecodeerd.
  • Reden voor de wijziging: Het belangrijkste doel van de optie string is het voorkomen van precisieverlies bij 64-bits gehele getallen door getallen als string weer te geven. Het gedrag van v1 was beperkt en inconsistent. v2 concentreert zich op dit kerndoel en breidt het gedrag recursief uit, zodat de optie nuttiger is.
  • Gerelateerde optie: Met de optie json.StringifyWithLegacySemantics(true) kan het gedrag van v1 worden nagebootst.

4. Marshalen van nil-slices en -maps

  • Gedrag van v1: nil-slices en nil-maps worden gemarshald als null.
  • Gedrag van v2: Standaard worden nil-slices gemarshald als [] (lege array) en nil-maps als {} (leeg object).
  • Reden voor de wijziging: nil is een implementatiedetail van de Go-taal, en het is niet wenselijk dit bloot te stellen in een taalonafhankelijk JSON-formaat. [] en {} zijn de meer algemene representaties van lege collecties.
  • Gerelateerde opties: In v2 kan met json.FormatNilSliceAsNull(true) of json.FormatNilMapAsNull(true) worden bereikt dat ze, zoals in v1, als null worden gemarshald.

5. Unmarshalen van arrays

  • Gedrag van v1: Bij het unmarshalen naar een Go-array ([N]T) levert een JSON-array met een andere lengte dan de Go-array geen fout op. Is de JSON-array korter, dan worden de overige posities met nulwaarden gevuld; is hij langer, dan wordt het surplus weggegooid.
  • Gedrag van v2: De lengte van de JSON-array moet exact overeenkomen met de lengte van de Go-array. Anders wordt een fout geretourneerd.
  • Reden voor de wijziging: In Go hebben vaste arrays vaak een betekenisvolle lengte. Het gedrag van v1 kan stil dataverlies veroorzaken. v2 hanteert strengere regels om de correctheid te verhogen.
  • Gerelateerde optie: Met de optie json.UnmarshalArrayFromAnyLength(true) kan het gedrag van v1 worden nagebootst.

6. Verwerking van time.Duration

  • Gedrag van v1: time.Duration wordt intern als int64 behandeld en gecodeerd als een JSON-getal in nanoseconden.
  • Gedrag van v2: Met de methode time.Duration.String() wordt gecodeerd als een JSON-string in een formaat zoals "1h2m3s".
  • Reden voor de wijziging: Getallen in nanoseconden zijn moeilijk leesbaar, en de standaard stringrepresentatie van time.Duration is nuttiger.
  • Gerelateerde opties: Met de tagoptie json:",format:nano" of de optie json.FormatTimeWithLegacySemantics(true) kan het gedrag van v1 worden gebruikt.

7. Verwerking van ongeldige UTF-8

  • Gedrag van v1: Bij marshalen/unmarshalen worden ongeldige UTF-8-bytes in strings stil vervangen door het Unicode-vervangingsteken (\uFFFD).
  • Gedrag van v2: Standaard wordt een fout geretourneerd wanneer ongeldige UTF-8 wordt aangetroffen.
  • Reden voor de wijziging: Dit voorkomt stille dataschade en sluit aan bij de strengere JSON-standaard (RFC 7493).
  • Gerelateerde optie: Met de optie jsontext.AllowInvalidUTF8(true) kan het gedrag van v1 worden nagebootst.

8. Verwerking van dubbele objectledennamen

  • Gedrag van v1: Een JSON-object mag leden met dezelfde naam bevatten. De laatst voorkomende waarde overschrijft de eerdere.
  • Gedrag van v2: Standaard wordt een fout geretourneerd wanneer dubbele ledennamen voorkomen.
  • Reden voor de wijziging: RFC 8259 definieert het gedrag bij dubbele namen niet, waardoor implementaties verschillend kunnen reageren. Dit kan een bron van beveiligingskwetsbaarheden zijn. v2 wijst dit expliciet af om correctheid en beveiliging te verhogen.
  • Gerelateerde optie: Met de optie jsontext.AllowDuplicateNames(true) kan het gedrag van v1 worden nagebootst.

Verschillen in implementatie en architectuur

  • v1: Vertrouwt sterk op decodeState in decode.go en op de handmatig geschreven state machine in scanner.go. Dit is een monolithische structuur waarin parsinglogica en semantische analyse sterk met elkaar verweven zijn.
  • v2: De architectuur is modularer.
    • encoding/json/jsontext: Biedt een laagniveau, hoogpresterende JSON-tokenizer (Decoder) en encoder (Encoder). Dit pakket richt zich uitsluitend op de syntactische aspecten van JSON.
    • encoding/json/v2: Verwerkt, op basis van jsontext, de semantische conversie tussen Go-typen en JSON-waarden.
    • Door deze scheiding zijn syntactische en semantische analyse van elkaar losgekoppeld, wat de duidelijkheid van de code en de prestaties heeft verbeterd.

Nieuwe API en functies in v2

v2 biedt via het optiesysteem `json.Options* een zeer flexibele besturing.

  • json.Options: Een verzameling opties die het marshal- en unmarshalgedrag wijzigen.
  • json.JoinOptions(...): Voegt meerdere opties samen tot één.
  • WithMarshalers / WithUnmarshalers: Een krachtige functie waarmee serialisatie- en deserialisatielogica voor specifieke typen kan worden geïnjecteerd zonder dat de interfaces Marshaler of Unmarshaler hoeven te worden geïmplementeerd. Dit is vooral nuttig bij het verwerken van typen uit externe pakketten.
  • Nieuwe opties: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull en andere opties maken een reeks gedragsinstellingen mogelijk die in v1 niet beschikbaar waren.

Conclusie

encoding/json v2 is een moderne implementatie die, voortbouwend op de ervaring met v1, correctheid, prestaties en flexibiliteit aanzienlijk verbetert. Het standaardgedrag is strenger geworden, maar via het geavanceerde Options-systeem worden alle gedragingen van v1 volledig ondersteund. Hierdoor kunnen bestaande code-compatibiliteit behouden blijven en kunnen de voordelen van v2 geleidelijk worden ingevoerd.

  • Voor nieuwe projecten is het aan te raden v2 als standaard te gebruiken.
  • Bestaande projecten kunnen jsonv1 ongewijzigd blijven gebruiken, of migreren naar jsonv2 en via DefaultOptionsV1() geleidelijk de strengere gedragingen van v2 invoeren.