Rund 15 Jahre nach dem ersten ausführlichen Beitrag zu JSON in Go bekommt die Sprache eine zweite JSON-API. Mit Go 1.27 sind encoding/json/v2 und encoding/json/jsontext stabil. Das Go-Team reagiert damit auf jahrelange Rückmeldungen aus der Praxis. Wer heute encoding/json verwendet, bekommt keine kleine Zusatzfunktion, sondern eine neu geordnete Grundlage für dasselbe Datenformat. Die alte API bleibt bestehen. Die neue setzt dort an, wo die bisherige Lösung Kompromisse eingehen musste.
Warum encoding/json an Grenzen stößt
Die alte API ist nicht schlecht. Die Idee, beliebige Go-Typen mit einer Standarddarstellung nach JSON zu übersetzen und bei Bedarf anzupassen, hat sich über viele Jahre bewährt. Die Autoren nennen aber eine Reihe von Verhaltensfehlern, die sich in dieser Form nicht mehr korrigieren lassen. encoding/json akzeptiert ungültiges UTF-8, obwohl RFC 8259 gültiges UTF-8 verlangt. Es akzeptiert JSON-Objekte mit doppelten Membernamen, obwohl deren Bedeutung nicht eindeutig ist. Daraus können Sicherheitsprobleme entstehen, wie CVE-2017-12635 zeigt. Hinzu kommen nil-Slices und nil-Maps, die als JSON-null geschrieben werden. Laut einer Umfrage hätten viele Go-Nutzer lieber ein leeres Array oder Objekt. Auch die standardmäßig case-insensitive Zuordnung von JSON-Namen zu Struct-Feldern gilt als überraschend, sicherheitsrelevant und langsam.
Dazu kommen API-Schwächen, die im Alltag oft unsichtbar bleiben. Wer aus einem io.Reader liest, schreibt oft json.NewDecoder(r).Decode(v) und merkt nicht, dass Reste am Ende der Eingabe nicht abgelehnt werden. Optionen lassen sich auf Encoder und Decoder setzen, aber nicht an Marshal oder Unmarshal übergeben. Typen, die Marshaler oder Unmarshaler implementieren, können Optionen nicht zuverlässig weiterreichen. Decoder.DisallowUnknownFields wirkt nicht mehr, sobald eine eigene UnmarshalJSON-Methode ins Spiel kommt. Compact, Indent und HTMLEscape schreiben in einen bytes.Buffer statt in []byte oder io.Writer. Das schränkt ihre Verwendbarkeit ein.
Auch bei der Performance setzt die öffentliche API Grenzen. MarshalJSON zwingt Implementierungen, ein Ergebnis als []byte zu allokieren. Anschließend muss encoding/json prüfen, ob das Ergebnis gültiges JSON ist, und es gegebenenfalls neu formatieren. UnmarshalJSON verlangt einen vollständigen JSON-Wert ohne nachfolgende Daten. Die Bibliothek muss also erst das gesamte Stück parsen, um sein Ende zu finden, bevor die Methode denselben Wert noch einmal parst. Trotz Encoder und Decoder wird der JSON-Wert vollständig im Speicher gepuffert. Decoder.Token erzeugt viele Allokationen, und eine passende Token-Schreib-API fehlt. Rekursive Aufrufe von Marshal oder Unmarshal innerhalb eigener Methoden können die Laufzeit quadratisch werden lassen. Eine direkte Reparatur von encoding/json wäre möglich, aber nicht im Rahmen des Go-1-Kompatibilitätsversprechens.
Syntax und Semantik: warum jsontext die neue Grundlage ist
Die neue Struktur lässt sich als zweistöckiges Haus beschreiben. Im Erdgeschoss kümmert sich jsontext ausschließlich um die Grammatik von JSON: Ist das, was da steht, überhaupt gültiges JSON? Wo beginnt und endet ein Wert? Wie werden Tokens gelesen und geschrieben? Im Obergeschoss sitzt encoding/json/v2 und gibt den syntaktischen Daten eine Bedeutung für Go-Typen. Die Autoren unterscheiden dafür ausdrücklich zwischen encode und decode auf der Syntaxseite sowie marshal und unmarshal auf der Semantikseite. Diese Trennung klingt akademisch, ist aber die Grundlage für die Verbesserungen.
jsontext ist nach Abschnitt 2 von RFC 8259 benannt, wo die Grammatik für JSON-Daten schlicht JSON-text heißt. Das Paket arbeitet nur auf der syntaktischen Ebene und benötigt keine Go-Reflection. Seine Encoder– und Decoder-Typen arbeiten streamend und nehmen Optionen als variadische Argumente entgegen. Ein JSON-Wert wird als benannter []byte-Typ repräsentiert und ist identisch mit dem bisherigen RawMessage, der nun ein Alias auf jsontext.Value ist. Ein JSON-Token ist ein opaker Token-Typ mit Konstruktoren und Zugriffsmethoden. Er ähnelt dem alten Token, ist aber so entworfen, dass beliebige Tokens ohne zusätzliche Allokation dargestellt werden können.
Die wichtigste Brücke zwischen beiden Etagen sind die neuen Schnittstellen MarshalJSONTo und UnmarshalJSONFrom. Sie arbeiten direkt auf einem jsontext.Encoder oder jsontext.Decoder und erlauben einer Implementierung, JSON streamend zu verarbeiten. Damit muss das json-Paket weder die Gültigkeit oder Formatierung eines zurückgegebenen JSON-Werts übernehmen noch die Grenzen eines übergebenen JSON-Werts bestimmen. Diese Aufgabe bleibt beim Encoder beziehungsweise Decoder. Das beseitigt einen großen Teil der alten Performanceprobleme, ohne die Semantik von JSON in Go neu zu erfinden.
Was encoding/json/v2 in Go 1.27 bietet
encoding/json/v2 baut auf jsontext auf und bleibt vertraut für alle, die encoding/json kennen. Marshal und Unmarshal haben ähnliche Signaturen wie bisher, akzeptieren aber Optionen zur Konfiguration ihres Verhaltens. MarshalWrite und UnmarshalRead arbeiten direkt mit einem io.Writer oder io.Reader. Man muss also nicht erst künstlich einen Encoder oder Decoder bauen. MarshalEncode und UnmarshalDecode wiederum arbeiten auf einem jsontext.Encoder oder jsontext.Decoder und bilden die eigentliche Grundlage der anderen Funktionen. Optionen sind damit normale Argumente und keine Randnotiz, die nur auf bestimmten Typen verfügbar ist.
Die typgesteuerte Anpassung bleibt. Marshaler und Unmarshaler sind identisch zu encoding/json. Zusätzlich gibt es MarshalerTo und UnmarshalerFrom, die einem Typ erlauben, sich über einen jsontext.Encoder oder jsontext.Decoder selbst als JSON darzustellen. Das ist wichtig, weil Optionen über die Options-Methode des Encoders oder Decoders weitergereicht werden können. Ein Typ kann also berücksichtigen, welche Einstellungen der aufrufende Code gesetzt hat, statt sie stillschweigend zu verlieren. Das Beispiel eines OrderedObject zeigt, wie sich damit sogar die Reihenfolge von JSON-Objektelementen erhalten lässt.
Auch auf der Aufruferseite gibt es mehr Kontrolle. Wer Marshal oder Unmarshal aufruft, kann für beliebige Typen eine eigene JSON-Darstellung vorgeben. MarshalFunc und MarshalToFunc erzeugen einen benutzerdefinierten Marshaler, den man über WithMarshalers an einen Aufruf übergibt. Solche aufruferspezifischen Funktionen haben Vorrang vor typdefinierten Methoden oder der Standarddarstellung. Das ist besonders nützlich, wenn eine Anwendung nur an einer Stelle ein bestimmtes Format braucht und dafür nicht den Typ selbst verändern möchte. Die neue API wird dadurch konfigurierbarer, ohne jede Sonderregel als globale Typentscheidung festzuschreiben.
Sicherere Defaults und die Frage der Rückwärtskompatibilität
Die neue API ist so angelegt, dass die Standardeinstellungen sicherer und berechenbarer sind. Ungültiges UTF-8 wird nicht mehr stillschweigend akzeptiert, doppelte Membernamen gehen nicht mehr als scheinbar eindeutige Daten durch. Nil-Slices und Nil-Maps erscheinen standardmäßig als leeres Array oder Objekt, statt Programme auf der anderen Seite mit null zu überraschen. Auch die case-insensitive Zuordnung von JSON-Namen zu Go-Feldern gehört zu den Entscheidungen, die die neue API neu trifft. Das sind keine kosmetischen Änderungen, sondern Korrekturen an Stellen, an denen JSON-Schnittstellen zwischen Programmen sonst unterschiedlich interpretiert werden können.
Für bestehenden Code bedeutet das nicht automatisch einen Bruch. Das Go-Team will, dass eine direkte Migration von encoding/json nach encoding/json/v2 in vielen Fällen weitgehend gleich funktioniert. Das Wort weitgehend ist wichtig, denn die sichereren Defaults können Programme treffen, die sich bisher auf großzügiges Verhalten verlassen haben. Wer eine Anwendung umstellt, sollte deshalb nicht nur kompilieren lassen, sondern Tests gegen reale Payloads laufen lassen. Besonders relevant sind Eingaben mit ungültigem UTF-8, doppelten Feldnamen, nil-Slices und gemischter Groß- und Kleinschreibung. Die alte API bleibt verfügbar, sodass eine Migration schrittweise möglich ist.
Der Gewinn bei der Performance liegt weniger in einem einzelnen Benchmark als in der Struktur. Streaming statt vollständiger Pufferung, weniger doppeltes Parsen, weniger erzwungene Allokationen und Optionen, die durch den Aufrufstack wandern, summieren sich bei großen oder verschachtelten JSON-Dokumenten. Bibliotheksautoren können die neuen Schnittstellen unterstützen, ohne ihre bisherigen Typen aufzugeben. Anwendungen profitieren dort, wo JSON nicht nur ein kleines Konfigurationsobjekt ist, sondern ein echter Datenstrom. Und JSON-Schnittstellen werden dort robuster, wo heute unterschiedliche Implementierungen dieselbe Eingabe verschieden auslegen könnten.
Was das für Go-Projekte und JSON-Schnittstellen bedeutet
Die neue API ist keine Revolution des Datenformats, sondern eine Korrektur der Übergabeschicht. JSON bleibt JSON, und Go bleibt Go. Geändert wird, wie sauber beide Seiten getrennt sind. jsontext prüft die Grammatik, encoding/json/v2 gibt den Daten Bedeutung. Diese Aufteilung macht die Bibliothek erklärbarer und erlaubt Optimierungen, die vorher an der öffentlichen API gescheitert wären. Gleichzeitig bleibt die alte encoding/json-Welt bestehen. Für große Codebasen und Bibliotheken ist das entscheidend.
Für dich als Entwickler heißt das: Du musst nicht sofort jede Zeile umstellen. Sinnvoll ist eine bewusste Entscheidung pro Projekt. Neue Dienste, die stark mit JSON arbeiten, können direkt auf encoding/json/v2 setzen und Optionen sowie Streaming-Schnittstellen von Anfang an nutzen. Bestehende Dienste sollten zuerst prüfen, ob sie sich auf die großzügigen Altlasten verlassen. Bibliotheken, die JSON-Typen anbieten, sollten perspektivisch MarshalerTo und UnmarshalerFrom unterstützen und Optionen nicht verschlucken. Wer JSON nur gelegentlich für kleine Konfigurationen nutzt, muss dagegen kaum sofort wechseln.
Go 1.27 bringt eine JSON-API, die viele bekannte Ärgernisse nicht mehr als dauerhafte Kompatibilitätslast mitschleppen muss. Sie ist vertraut genug für einen direkten Einstieg, aber an den entscheidenden Stellen präziser, sicherer und besser konfigurierbar. Ob ein konkretes Projekt profitiert, hängt davon ab, wie es JSON einsetzt und wie viel Vertrauen es in seine Eingabedaten hat. Die neue Standardbibliothek liefert dafür eine bessere Grundlage, ohne die alte Welt abzuschalten.
Quelle: go.dev
