Analiza zmian w działaniu encoding/json/v2 w Go 1.25 oraz warstwy zgodności z v1

Pakiet encoding/json w wersji v2 w Go to nowa implementacja, której celem jest usunięcie kilku wad wersji v1 (brak spójności, zaskakujące zachowania, problemy z wydajnością). Jest to funkcja eksperymentalna, aktywowana za pomocą znacznika kompilacji goexperiment.jsonv2.
Najważniejszy jest fakt, że po aktywacji v2 wersja v1 działa jako warstwa zgodności, która emuluje zachowanie v1 na bazie implementacji v2. Odbywa się to za pomocą funkcji DefaultOptionsV1() z pliku v2_v1.go. Innymi słowy, v2 udostępnia opcje pozwalające w pełni odtworzyć zachowanie v1, a jednocześnie proponuje bardziej rygorystyczne i przewidywalne nowe domyślne zachowanie.
Główne cele v2 są następujące:
- Poprawa poprawności i przewidywalności: Domyślnie stosowane są bardziej rygorystyczne reguły (np. rozróżnianie wielkości liter, zakaz zduplikowanych kluczy), co ogranicza nieoczekiwane zachowania.
- Poprawa wydajności: Silniki parsowania i kodowania zostały przeprojektowane w celu zwiększenia efektywności.
- Zwiększenie elastyczności i kontroli: Wprowadzono szczegółowy system opcji, który pozwala programistom precyzyjnie kontrolować sposób przetwarzania JSON.
Główne różnice w znaczeniu i zachowaniu
Poniżej zestawiono różnice w zachowaniu między v1 a v2 na podstawie pliku v2_test.go.
1. Dopasowywanie nazw pól (rozróżnianie wielkości liter)
- Zachowanie v1: Podczas unmarshalowania członków obiektu JSON do pól struktury Go dopasowanie odbywa się bez rozróżniania wielkości liter (case-insensitive). Zarówno
"FirstName", jak i"firstname"zostaną zmapowane na poleFirstName. - Zachowanie v2: Domyślnie dopasowywanie rozróżnia wielkość liter (case-sensitive) i mapuje wyłącznie pola o dokładnie zgodnej nazwie.
- Uzasadnienie zmiany: Dopasowanie bez rozróżniania wielkości liter może być źródłem nieoczekiwanych zachowań i powodować spadek wydajności przy obsłudze niedopasowanych pól. v2 przyjmuje za domyślne zachowanie jaśniejsze i bardziej przewidywalne.
- Powiązane opcje: W v2 można jawnie włączyć ignorowanie wielkości liter dla pojedynczego pola za pomocą opcji znacznika
json:"...,case:ignore"lub zastosować globalnie opcjęjson.MatchCaseInsensitiveNames(true).
2. Zmiana znaczenia opcji znacznika omitempty
- Zachowanie v1: Pole jest pomijane na podstawie „stanu pustego” wartości Go. Oznacza to
false,0, wskaźnik lub interfejsnil, a także tablicę, wycinek, mapę lub łańcuch o długości zero. - Zachowanie v2: Pole jest pomijane na podstawie „stanu pustego” zakodowanej wartości JSON. Oznacza to, że pole zostanie pominięte, gdy zostanie zakodowane jako
null,"",{}lub[]. - Uzasadnienie zmiany: Definicja v1 zależy od systemu typów Go. v2 opiera się na systemie typów JSON, zapewniając bardziej spójne zachowanie. Na przykład w v1 wartość
falsetypubooljest pomijana, natomiast w v2falsejest niepustą wartością JSON i nie zostaje pominięta. W v2 dodano opcjęomitzero, która może zastąpić dotychczasowe działanieomitemptydla0ifalse. - Powiązane opcje: Aby w v2 uzyskać zachowanie identyczne z v1, należy użyć opcji
json.OmitEmptyWithLegacyDefinition(true).
3. Zmiana działania opcji string
- Zachowanie v1: Dotyczy pól typu liczbowego, logicznego i łańcuchowego. Wartość jest ponownie kodowana wewnątrz łańcucha JSON (np.
int(42)->"42"). Nie jest stosowana rekurencyjnie do wartości wewnątrz typów złożonych (wycinków, map itp.). - Zachowanie v2: Opcja działa wyłącznie dla typów liczbowych i jest stosowana rekurencyjnie. Oznacza to, że liczby wewnątrz wycinka, np.
[]int, również są kodowane jako łańcuchy JSON. - Uzasadnienie zmiany: Główne zastosowanie opcji
stringto zapobieganie utracie precyzji 64-bitowych liczb całkowitych poprzez przedstawianie liczb jako łańcuchów. Zachowanie v1 było ograniczone i niespójne. v2 koncentruje się na tym kluczowym zastosowaniu i rozszerza działanie rekurencyjnie, czyniąc je bardziej użytecznym. - Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji
json.StringifyWithLegacySemantics(true).
4. Marshalowanie wartości nil dla wycinków i map
- Zachowanie v1: Wycinki
nili mapynilsą marshalowane jakonull. - Zachowanie v2: Domyślnie wycinek
niljest marshalowany jako[](pusta tablica), a mapaniljako{}(pusty obiekt). - Uzasadnienie zmiany:
niljest szczegółem implementacyjnym języka Go, a ujawnianie go w formacie JSON, który nie zależy od języka, nie jest pożądane. Oznaczenia[]i{}stanowią powszechniejszy sposób reprezentacji pustych kolekcji. - Powiązane opcje: W v2 można uzyskać marshalowanie jako
null, tak jak w v1, używając opcjijson.FormatNilSliceAsNull(true)lubjson.FormatNilMapAsNull(true).
5. Unmarshalowanie tablic
- Zachowanie v1: Podczas unmarshalowania do tablicy Go (
[N]T) długość tablicy JSON może różnić się od długości tablicy Go bez wywołania błędu. Jeśli tablica JSON jest krótsza, pozostałe miejsca są wypełniane wartościami zerowymi; jeśli jest dłuższa, nadmiarowe elementy są odrzucane. - Zachowanie v2: Długość tablicy JSON musi dokładnie odpowiadać długości tablicy Go. W przeciwnym razie zgłaszany jest błąd.
- Uzasadnienie zmiany: W Go długość tablic o stałym rozmiarze często ma istotne znaczenie. Zachowanie v1 może prowadzić do cichej utraty danych. v2 stosuje bardziej rygorystyczną zasadę, co zwiększa poprawność.
- Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji
json.UnmarshalArrayFromAnyLength(true).
6. Obsługa time.Duration
- Zachowanie v1:
time.Durationjest wewnętrznie traktowany jakoint64i kodowany jako liczba JSON w nanosekundach. - Zachowanie v2: Wartość jest kodowana jako łańcuch JSON w formacie takim jak
"1h2m3s", z wykorzystaniem metodytime.Duration.String(). - Uzasadnienie zmiany: Liczby w nanosekundach są słabo czytelne, a standardowa reprezentacja łańcuchowa
time.Durationjest bardziej użyteczna. - Powiązane opcje: Zachowanie v1 można uzyskać za pomocą opcji znacznika
json:",format:nano"lub opcjijson.FormatTimeWithLegacySemantics(true).
7. Obsługa nieprawidłowego UTF-8
- Zachowanie v1: Podczas marshalowania i unmarshalowania nieprawidłowe bajty UTF-8 wewnątrz łańcuchów są po cichu zastępowane znakiem zastępczym Unicode (
\uFFFD). - Zachowanie v2: Domyślnie napotkanie nieprawidłowego UTF-8 powoduje zwrócenie błędu.
- Uzasadnienie zmiany: Ma to na celu zapobieganie cichemu uszkadzaniu danych oraz zgodność z bardziej rygorystycznym standardem JSON (RFC 7493).
- Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji
jsontext.AllowInvalidUTF8(true).
8. Obsługa zduplikowanych nazw członków obiektu
- Zachowanie v1: Obiekt JSON może zawierać członków o tej samej nazwie. Ostatnia wartość nadpisuje poprzednie.
- Zachowanie v2: Domyślnie występowanie zduplikowanej nazwy członka powoduje zwrócenie błędu.
- Uzasadnienie zmiany: Standard RFC 8259 nie definiuje zachowania dla zduplikowanych nazw, przez co różne implementacje mogą działać odmiennie. Może to być przyczyną luk bezpieczeństwa. v2 jawnie odrzuca takie dane, zwiększając poprawność i bezpieczeństwo.
- Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji
jsontext.AllowDuplicateNames(true).
Różnice w implementacji i architekturze
- v1: Silnie opiera się na
decodeStatew plikudecode.gooraz ręcznie napisanej maszynie stanów wscanner.go. Jest to monolityczna struktura, w której logika parsowania jest silnie powiązana z analizą semantyczną. - v2: Architektura jest bardziej modularna.
encoding/json/jsontext: Udostępnia niskopoziomowy, wydajny tokenizator JSON (Decoder) oraz koder (Encoder). Pakiet ten koncentruje się wyłącznie na składni JSON.encoding/json/v2: Opierając się najsontext, obsługuje semantyczne przekształcenia między typami Go a wartościami JSON.- Dzięki takiemu rozdzieleniu analiza składniowa i semantyczna są od siebie oddzielone, co poprawia czytelność kodu i wydajność.
Nowe API i funkcje v2
v2 zapewnia bardzo elastyczne sterowanie za pomocą systemu json.Options.
json.Options: Zbiór opcji zmieniających zachowanie marshalowania i unmarshalowania.json.JoinOptions(...): Łączy wiele opcji w jedną.WithMarshalers/WithUnmarshalers: Potężna funkcja, która pozwala wstrzyknąć logikę serializacji i deserializacji dla konkretnego typu bez implementowania interfejsówMarshaleriUnmarshaler. Jest szczególnie przydatna przy obsłudze typów z zewnętrznych pakietów.- Nowe opcje:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNulli inne, które umożliwiają kontrolę zachowania niedostępną w v1.
Podsumowanie
encoding/json v2 to nowoczesna implementacja, która na podstawie doświadczeń z v1 znacząco poprawia poprawność, wydajność i elastyczność. Domyślne zachowanie stało się bardziej rygorystyczne, jednak dzięki zaawansowanemu systemowi Options możliwe jest w pełni odtworzenie wszystkich zachowań v1, co pozwala zachować zgodność z istniejącym kodem i stopniowo wdrażać zalety v2.
- Nowe projekty powinny domyślnie korzystać z v2.
- Istniejące projekty mogą nadal używać
jsonv1bez zmian lub migrować dojsonv2, stosującDefaultOptionsV1()i stopniowo wprowadzać rygorystyczne zachowanie v2.