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

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.
- Verbetering van correctheid en voorspelbaarheid: Door standaard strengere regels toe te passen (bijvoorbeeld hoofdlettergevoeligheid en verboden dubbele sleutels) worden onverwachte gedragingen beperkt.
- Prestatieverbetering: De parsing- en coderingsengine is opnieuw ontworpen om de efficiëntie te verhogen.
- 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 veldFirstNamegekoppeld. - 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 optiejson.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
falsevan eenbool-veld weggelaten, terwijl in v2falseeen niet-lege JSON-waarde is en dus niet wordt weggelaten. In v2 is de optieomitzerotoegevoegd, waarmee het gedrag vanomitemptyvan v1 voor0offalsekan 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
[]intallemaal als JSON-string worden gecodeerd. - Reden voor de wijziging: Het belangrijkste doel van de optie
stringis 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 ennil-maps worden gemarshald alsnull. - Gedrag van v2: Standaard worden
nil-slices gemarshald als[](lege array) ennil-maps als{}(leeg object). - Reden voor de wijziging:
nilis 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)ofjson.FormatNilMapAsNull(true)worden bereikt dat ze, zoals in v1, alsnullworden 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.Durationwordt intern alsint64behandeld 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.Durationis nuttiger. - Gerelateerde opties: Met de tagoptie
json:",format:nano"of de optiejson.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
decodeStateindecode.goen op de handmatig geschreven state machine inscanner.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 vanjsontext, 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 interfacesMarshalerofUnmarshalerhoeven te worden geïmplementeerd. Dit is vooral nuttig bij het verwerken van typen uit externe pakketten.- Nieuwe opties:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNullen 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
jsonv1ongewijzigd blijven gebruiken, of migreren naarjsonv2en viaDefaultOptionsV1()geleidelijk de strengere gedragingen van v2 invoeren.