GoSuda

Analys av beteendeförändringar i Go 1.25 encoding/json/v2 och kompatibilitetslagret för v1

By lemonmint views ...

Paketet encoding/json v2 i Go är en ny implementation som syftar till att förbättra flera brister i den befintliga v1 (bristande konsekvens, överraskande beteenden, prestandaproblem). Den aktiveras via byggtaggen goexperiment.jsonv2 och är en experimentell funktion.

Det viktigaste är att när v2 är aktiverat fungerar v1 som ett kompatibilitetslager ovanpå v2-implementationen, som emulerar v1:s beteende. Detta sker via funktionen DefaultOptionsV1() i filen v2_v1.go. Det innebär att v2 tillhandahåller alternativ som helt kan återskapa v1:s beteende, samtidigt som den erbjuder nya standardbeteenden som är strängare och mer förutsägbara.

Huvudmålen för v2 är följande.

  1. Förbättrad korrekthet och förutsägbarhet: Genom strängare standardregler (till exempel skiftlägeskänslighet och förbud mot dubbletter av nycklar) minskas oväntade beteenden.
  2. Förbättrad prestanda: Parsnings- och kodningsmotorerna har konstruerats om för ökad effektivitet.
  3. Ökad flexibilitet och kontroll: Ett detaljerat alternativsystem införs så att utvecklare kan finjustera hur JSON-bearbetningen sker.

Viktiga semantiska och beteendemässiga skillnader

Skillnaderna mellan v1 och v2 sammanfattas nedan utifrån filen v2_test.go.

1. Matchning av fältnamn (skiftlägeskänslighet)

  • v1-beteende: Vid avmarshalling av JSON-objektmedlemmar till Go-structfält ignoreras skiftläge (case-insensitive) vid matchningen. Både "FirstName" och "firstname" mappas till fältet FirstName.
  • v2-beteende: Som standard är matchningen skiftlägeskänslig (case-sensitive) och mappar endast fält som matchar exakt.
  • Orsak till ändringen: Skiftlägesoberoende matchning kan orsaka oväntade beteenden och medför prestandaförluster när icke-matchande fält ska hanteras. v2 har därför antagit ett tydligare och mer förutsägbart standardbeteende.
  • Relaterade alternativ: I v2 kan skiftlägesoberoende matchning aktiveras explicit för enskilda fält med taggalternativet json:"...,case:ignore", eller globalt genom alternativet json.MatchCaseInsensitiveNames(true).

2. Förändrad betydelse av taggalternativet omitempty

  • v1-beteende: Fält utelämnas utifrån Go-värdets "tomma tillstånd". Med "tomt tillstånd" avses false, 0, nil-pekare eller gränssnitt samt arrayer, slice:ar, maps och strängar med längden noll.
  • v2-beteende: Fält utelämnas utifrån det kodade JSON-värdets "tomma tillstånd". Det innebär att fältet utelämnas om det kodas som null, "", {} eller [].
  • Orsak till ändringen: v1:s definition är beroende av Go:s typsystem. v2 utgår i stället från JSON:s typsystem och ger därför ett mer konsekvent beteende. Till exempel utelämnas i v1 värdet false för en bool, medan det i v2 inte utelämnas eftersom false är ett icke-tomt JSON-värde. I v2 kan alternativet omitzero användas för att ersätta v1:s beteende där omitempty gällde för 0 eller false.
  • Relaterade alternativ: För att i v2 få samma beteende som i v1 används alternativet json.OmitEmptyWithLegacyDefinition(true).

3. Förändrat beteende för taggalternativet string

  • v1-beteende: Tillämpas på fält av typerna tal, booleska värden och strängar. Värdet kodas på nytt inuti en JSON-sträng (till exempel int(42) -> "42"). Det tillämpas inte rekursivt på värden inuti sammansatta typer (slice:ar, maps med mera).
  • v2-beteende: Tillämpas endast på numeriska typer och tillämpas rekursivt. Det innebär att även tal inuti slice:ar, till exempel []int, kodas som JSON-strängar.
  • Orsak till ändringen: Det huvudsakliga syftet med alternativet string är att förhindra precisionsförlust för 64-bitars heltal genom att representera talen som strängar. v1:s beteende var begränsat och inkonsekvent. v2 koncentrerar sig på detta kärnsyfte och utökar beteendet rekursivt för att göra det mer användbart.
  • Relaterade alternativ: Med alternativet json.StringifyWithLegacySemantics(true) kan v1:s beteende efterliknas.

4. Marshalling av nil-slice:ar och maps

  • v1-beteende: nil-slice:ar och nil-maps marshalleras som null.
  • v2-beteende: Som standard marshalleras nil-slice:ar som [] (tom array) och nil-maps som {} (tomt objekt).
  • Orsak till ändringen: nil är en implementationsdetalj i Go-språket, och det är inte önskvärt att exponera detta i ett språkoberoende JSON-format. [] och {} är vanligare representationer av tomma samlingar.
  • Relaterade alternativ: I v2 kan marshalling till null, likt v1, uppnås med alternativen json.FormatNilSliceAsNull(true) eller json.FormatNilMapAsNull(true).

5. Avmarshalling av arrayer

  • v1-beteende: Vid avmarshalling till Go-arrayer ([N]T) ger en JSON-array vars längd avviker från Go-arrayens längd inget fel. Om JSON-arrayen är kortare fylls de återstående platserna med nollvärden, och om den är längre kastas det överskjutande bort.
  • v2-beteende: JSON-arrayens längd måste exakt överensstämma med Go-arrayens längd. Annars uppstår ett fel.
  • Orsak till ändringen: I Go har arrayer med fast storlek ofta en betydelsefull längd. v1:s beteende kan orsaka tyst dataförlust. v2 har genom striktare regler ökat korrektheten.
  • Relaterade alternativ: Med alternativet json.UnmarshalArrayFromAnyLength(true) kan v1:s beteende efterliknas.

6. Hantering av time.Duration

  • v1-beteende: time.Duration behandlas internt som int64 och kodas som ett JSON-tal i nanosekunder.
  • v2-beteende: Kodas som en JSON-sträng i formatet "1h2m3s" med hjälp av metoden time.Duration.String().
  • Orsak till ändringen: Tal i nanosekunder är svåra att läsa, och den standardiserade strängrepresentationen av time.Duration är mer användbar.
  • Relaterade alternativ: v1:s beteende kan användas med taggalternativet json:",format:nano" eller alternativet json.FormatTimeWithLegacySemantics(true).

7. Hantering av ogiltig UTF-8

  • v1-beteende: Vid marshalling och avmarshalling ersätts tyst ogiltiga UTF-8-bytes i strängar med Unicodes ersättningstecken (\uFFFD).
  • v2-beteende: Som standard returneras ett fel när ogiltig UTF-8 påträffas.
  • Orsak till ändringen: Syftet är att förhindra tyst datakorruption och att följa den striktare JSON-standarden (RFC 7493).
  • Relaterade alternativ: Med alternativet jsontext.AllowInvalidUTF8(true) kan v1:s beteende efterliknas.

8. Hantering av dubbla medlemsnamn i objekt

  • v1-beteende: JSON-objekt får innehålla medlemmar med samma namn. Det sist förekommande värdet skriver över de tidigare.
  • v2-beteende: Som standard returneras ett fel om dubbla medlemsnamn förekommer.
  • Orsak till ändringen: RFC 8259 definierar inte beteendet vid dubbla namn, vilket kan leda till olika beteenden mellan implementationer. Detta kan vara en källa till säkerhetsbrister. v2 avvisar därför dubbletterna explicit för ökad korrekthet och säkerhet.
  • Relaterade alternativ: Med alternativet jsontext.AllowDuplicateNames(true) kan v1:s beteende efterliknas.

Skillnader i implementation och arkitektur

  • v1: Förlitar sig i hög grad på decodeState i decode.go och en manuellt skriven tillståndsmaskin (state machine) i scanner.go. Detta är en monolitisk struktur där parsningslogik och semantisk analys är starkt kopplade.
  • v2: Arkitekturen är mer modulär.
    • encoding/json/jsontext: Tillhandahåller en lågnivå-tokenizer (Decoder) och kodare (Encoder) med hög prestanda. Paketet fokuserar enbart på JSON:s syntaktiska aspekter.
    • encoding/json/v2: Bygger på jsontext och hanterar den semantiska omvandlingen mellan Go-typer och JSON-värden.
    • Genom denna uppdelning separeras syntaktisk analys från semantisk analys, vilket förbättrar kodens tydlighet och prestanda.

Nya API:er och funktioner i v2

v2 erbjuder mycket flexibel kontroll via systemet json.Options.

  • json.Options: En samling alternativ som ändrar marshalling- och avmarshallingbeteendet.
  • json.JoinOptions(...): Slår samman flera alternativ till ett.
  • WithMarshalers / WithUnmarshalers: Ett kraftfullt sätt att injicera serialiserings- och deserialiseringslogik för specifika typer, utan att typerna behöver implementera gränssnitten Marshaler eller Unmarshaler. Detta är särskilt användbart vid hantering av typer från externa paket.
  • Nya alternativ: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull med flera ger kontroll över beteenden som inte var möjliga i v1.

Slutsats

encoding/json v2 är en modern implementation som, med utgångspunkt i erfarenheterna från v1, avsevärt förbättrar korrekthet, prestanda och flexibilitet. Standardbeteendet har blivit strängare, men genom det avancerade systemet Options kan v1:s alla beteenden fullt ut återskapas. Det gör det möjligt att behålla kompatibilitet med befintlig kod och successivt införa v2:s fördelar.

  • För nya projekt rekommenderas att v2 används som standard.
  • Befintliga projekt kan fortsätta använda jsonv1 som tidigare, eller migrera till jsonv2 och successivt införa v2:s strängare beteende med hjälp av DefaultOptionsV1().