ProtoJSON Format

Beschreibt die Verwendung der Konvertierungs-Utilities von Protobuf zu JSON.

Protobuf unterstützt eine kanonische Kodierung in JSON, was den Austausch von Daten mit Systemen erleichtert, die das Standard-Binär-Wire-Format von Protobuf nicht unterstützen.

Diese Seite spezifiziert das Format, aber eine Reihe zusätzlicher Randfälle, die einen konformen ProtoJSON-Parser definieren, sind in der Protobuf Conformance Test Suite abgedeckt und werden hier nicht erschöpfend detailliert.

Nicht-Ziele des Formats

Einige JSON-Schemata können nicht dargestellt werden

Das ProtoJSON-Format ist als JSON-Darstellung von Schemata konzipiert, die in der Protobuf-Schemasprache ausgedrückt werden können.

Es ist möglicherweise möglich, viele bereits existierende JSON-Schemata als Protobuf-Schema darzustellen und sie mit ProtoJSON zu parsen, aber es ist nicht dafür konzipiert, beliebige JSON-Schemata darstellen zu können.

Zum Beispiel gibt es im Protobuf-Schema keine Möglichkeit, Typen zu schreiben, die in JSON-Schemata üblich sein können, wie z. B. number[][] oder number|string.

Es ist möglich, google.protobuf.Struct und google.protobuf.Value Typen zu verwenden, um beliebige JSON in ein Protobuf-Schema zu parsen, aber diese erlauben nur, die Werte als schemalose, unsortierte Schlüssel-Wert-Paare zu erfassen.

Nicht so effizient wie das binäre Wire-Format

Das ProtoJSON-Format ist nicht so effizient wie das binäre Wire-Format und wird es auch nie sein.

Der Konverter benötigt mehr CPU zum Kodieren und Dekodieren von Nachrichten und (außer in seltenen Fällen) verbrauchen kodierte Nachrichten mehr Speicherplatz.

Hat nicht die gleichen Schema-Evolutionsgarantien wie das binäre Wire-Format

Das ProtoJSON-Format unterstützt keine unbekannten Felder und fügt Feld- und Enum-Wertnamen in die kodierten Nachrichten ein, was es viel schwieriger macht, diese Namen später zu ändern. Das Entfernen von Feldern ist eine "breaking change", die einen Parsing-Fehler auslöst.

Weitere Details finden Sie unter JSON Wire-Sicherheit.

Formatbeschreibung

Darstellung jedes Typs

Die folgende Tabelle zeigt, wie Daten in JSON-Dateien dargestellt werden.

ProtobufJSONJSON-BeispielHinweise
messageobject{"fooBar": v, "g": null, ...}Erzeugt JSON-Objekte. Nachrichtenfeldnamen werden auf lowerCamelCase abgebildet und werden zu JSON-Objektschlüsseln. Wenn die Feldoption json_name angegeben ist, wird der angegebene Wert stattdessen als Schlüssel verwendet. Parser akzeptieren sowohl den lowerCamelCase-Namen (oder den durch die json_name-Option angegebenen) als auch den ursprünglichen Proto-Feldnamen. null ist ein akzeptierter Wert für alle Feldtypen und lässt das Feld unbesetzt. \0 (Nul-Zeichen) kann nicht innerhalb eines json_name-Werts verwendet werden. Warum, siehe Strengere Validierung für json_name.
enumstring"FOO_BAR"Der im Proto angegebene Name des Enum-Werts wird verwendet. Parser akzeptieren sowohl Enum-Namen als auch Ganzzahlwerte.
map<K,V>object{"k": v, ...}Alle Schlüssel werden in Strings konvertiert (Schlüssel in der JSON-Spezifikation können nur Strings sein).
repeated Varray[v, ...]null wird als leeres Array [] akzeptiert.
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64-String"YWJjMTIzIT8kKiYoKSctPUB+"Der JSON-Wert ist die als String kodierte Datendarstellung unter Verwendung der Standard-Base64-Kodierung mit Auffüllung. Sowohl Standard- als auch URL-sichere Base64-Kodierung mit/ohne Auffüllung werden akzeptiert.
int32, fixed32, uint32number1, -10, 0Der JSON-Wert ist eine Dezimalzahl. Entweder Zahlen oder Strings sind akzeptiert. Leere Strings sind ungültig. Exponentenschreibweise (wie z. B. `1e2`) ist sowohl in Anführungszeichen als auch ohne Anführungszeichen akzeptiert.
int64, fixed64, uint64string"1", "-10"Der JSON-Wert ist ein Dezimal-String. Entweder Zahlen oder Strings sind akzeptiert. Leere Strings sind ungültig. Exponentenschreibweise (wie z. B. `1e2`) ist sowohl in Anführungszeichen als auch ohne Anführungszeichen akzeptiert.
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"Der JSON-Wert ist eine Zahl oder einer der speziellen String-Werte "NaN", "Infinity" und "-Infinity". Sowohl Zahlen als auch Strings sind akzeptiert. Leere Strings sind ungültig. Exponentenschreibweise ist ebenfalls akzeptiert.
Anyobject{"@type": "url", "f": v, ... }Wenn das Any-Objekt einen bekannten Typ enthält, der eine spezielle JSON-Abbildung in dieser Tabelle hat (z. B. google.protobuf.Duration), wird es wie folgt konvertiert: {"@type": xxx, "value": yyy}. Andernfalls wird der Wert wie üblich in ein JSON-Objekt konvertiert, und ein zusätzliches Feld "@type" wird mit einem Wert der URL, die den Typ der Nachricht angibt, eingefügt.
Timestampstring"1972-01-01T10:00:20.021Z"Verwendet RFC 3339 (siehe Klarstellung), wobei die generierte Ausgabe immer Z-normalisiert ist und 0, 3, 6 oder 9 Nachkommastellen verwendet. Abweichungen außer "Z" werden ebenfalls akzeptiert.
Durationstring"1.000340012s", "1s"Die generierte Ausgabe enthält immer 0, 3, 6 oder 9 Nachkommastellen, abhängig von der benötigten Präzision, gefolgt vom Suffix "s". Akzeptiert werden beliebige Nachkommastellen (auch keine), solange sie in die Nanosekundenpräzision passen und das Suffix "s" erforderlich ist.
Structobject{ ... }Beliebiges JSON-Objekt. Siehe struct.proto.
Wrapper-Typenverschiedene Typen2, "2", "foo", true, "true", null, 0, ...Wrapper verwenden die gleiche Darstellung in JSON wie der umschlossene primitive Typ, außer dass null zulässig ist und während der Datenkonvertierung und -übertragung beibehalten wird.
FieldMaskstring"f.fooBar,h"Siehe field_mask.proto.
ListValuearray[foo, bar, ...]
ValuevalueBeliebiger JSON-Wert. Überprüfen Sie google.protobuf.Value für Details.
NullValuenullJSON-null. Sonderfall des [Null-Parsing-Verhaltens](#null-values).
Emptyobject{}Ein leeres JSON-Objekt

Präsenz und Standardwerte

Beim Generieren von JSON-kodiertem Output aus einem Protokollpuffer, wenn ein Feld die Präsenz unterstützt, müssen Serialisierer den Feldwert ausgeben, genau dann, wenn der entsprechende "Hasser" `true` zurückgeben würde.

Wenn das Feld keine Feldpräsenz unterstützt und den Standardwert hat (z. B. ein leeres wiederholtes Feld), sollten Serialisierer es aus der Ausgabe weglassen. Eine Implementierung kann Optionen anbieten, um Felder mit Standardwerten in die Ausgabe aufzunehmen.

Null-Werte

Serialisierer sollten keine null-Werte ausgeben.

Parser sollten null als gültigen Wert für jedes Feld akzeptieren, mit dem Verhalten

  • Eine Schlüsselvalidierung sollte weiterhin stattfinden (unbekannte Felder sind nicht erlaubt)
  • Das Feld sollte unbesetzt bleiben, als ob es überhaupt nicht in der Eingabe vorhanden wäre (Hasser sollten weiterhin `false` zurückgeben, wo zutreffend).

null-Werte sind in wiederholten Feldern nicht erlaubt.

google.protobuf.NullValue ist eine spezielle Ausnahme von diesem Verhalten: null wird als sentinel-präsentierter Wert für diesen Typ behandelt, und daher muss ein Feld dieses Typs von Serialisierern und Parsen unter dem Standard-Präsenzverhalten behandelt werden. Dieses Verhalten erlaubt es entsprechend google.protobuf.Struct und google.protobuf.Value, beliebige JSON verlustfrei zu roundtrippen.

Doppelte Werte

Serialisierer dürfen niemals dasselbe Feld mehrmals serialisieren, noch mehrere verschiedene Fälle in demselben `oneof` in demselben JSON-Objekt.

Parser sollten das Duplizieren desselben Feldes akzeptieren, und der zuletzt angegebene Wert sollte beibehalten werden. Dies gilt auch für „alternative Schreibweisen“ desselben Feldnamens.

Wenn Implementierungen die notwendigen Informationen über die Feldreihenfolge nicht beibehalten können, ist es vorzuziehen, Eingaben mit doppelten Schlüsseln abzulehnen, anstatt einen beliebigen Wert gewinnen zu lassen. In einigen Implementierungen kann die Beibehaltung der Feldreihenfolge von Objekten unpraktisch oder nicht machbar sein, daher wird dringend empfohlen, dass Systeme nach Möglichkeit keine spezifischen Verhaltensweisen für doppelte Felder in ProtoJSON erwarten.

Numerische Werte außerhalb des Bereichs

Beim Parsen eines numerischen Werts, wenn die Zahl, die vom Wire gelesen wird, nicht in den entsprechenden Typ passt, sollte der Parser den Wert in den passenden Typ umwandeln. Dies hat das gleiche Verhalten wie eine einfache Typumwandlung in C++ oder Java (zum Beispiel, wenn eine Zahl größer als 2^32 als `int32` Feld gelesen wird, wird sie auf 32 Bits gekürzt).

ProtoJSON Wire-Sicherheit

Bei Verwendung von ProtoJSON sind nur einige Schemaänderungen sicher in einem verteilten System durchzuführen. Dies steht im Gegensatz zu den gleichen Konzepten, die auf das binäre Wire-Format angewendet werden.

Wire-unsichere Änderungen in JSON

Wire-unsichere Änderungen sind Schemaänderungen, die fehlschlagen, wenn Sie Daten parsen, die mit dem alten Schema serialisiert wurden, mit einem Parser, der das neue Schema verwendet (oder umgekehrt). Diese Art von Schemaänderung sollten Sie fast nie durchführen.

  • Das Ändern eines Feldes zu oder von einer Erweiterung mit derselben Nummer und demselben Typ ist nicht sicher.
  • Das Ändern eines Feldes zwischen string und bytes ist nicht sicher.
  • Das Ändern eines Feldes zwischen einem Nachrichtentyp und bytes ist nicht sicher.
  • Das Ändern eines beliebigen Feldes von optional zu repeated ist nicht sicher.
  • Das Ändern eines Feldes zwischen einer map<K, V> und dem entsprechenden repeated Nachrichtenfeld ist nicht sicher.
  • Das Verschieben von Feldern in eine vorhandene oneof ist nicht sicher.

Wire-sichere Änderungen in JSON

Wire-sichere Änderungen sind solche, bei denen es völlig sicher ist, das Schema auf diese Weise zu entwickeln, ohne Risiko von Datenverlust oder neuen Parsing-Fehlern.

Beachten Sie, dass fast alle wire-sicheren Änderungen zu "breaking changes" für den Anwendungscode führen können. Zum Beispiel wäre das Hinzufügen eines Werts zu einem bestehenden Enum ein Kompilierungsfehler für jeden Code mit einer erschöpfenden Schalteranweisung für dieses Enum. Aus diesem Grund vermeidet Google möglicherweise einige dieser Arten von Änderungen an öffentlichen Nachrichten. Die AIPs enthalten Anleitungen, welche dieser Änderungen dort sicher vorgenommen werden können.

  • Das Ändern eines einzelnen optional-Feldes in ein Mitglied eines **neuen** oneof ist sicher.
  • Das Ändern eines oneof, das nur ein Feld enthält, in ein optional-Feld ist sicher.
  • Das Ändern eines Feldes zwischen beliebigen der Typen int32, sint32, sfixed32, fixed32 ist sicher.
  • Das Ändern eines Feldes zwischen beliebigen der Typen int64, sint64, sfixed64, fixed64 ist sicher.
  • Das Ändern einer Feldnummer ist sicher (da die Feldnummern im ProtoJSON-Format nicht verwendet werden), wird aber dennoch dringend abgeraten, da es im binären Wire-Format sehr unsicher ist.
  • Das Hinzufügen von Werten zu einem Enum ist sicher, wenn die Option "Enum-Werte als Ganzzahlen ausgeben" auf allen relevanten Clients gesetzt ist (siehe Optionen)

Wire-kompatible Änderungen in JSON (bedingt sicher)

Im Gegensatz zu wire-sicheren Änderungen bedeutet wire-kompatibel, dass dieselben Daten sowohl vor als auch nach einer bestimmten Änderung geparst werden können. Ein Client, der sie liest, erhält jedoch unter dieser Art von Änderung verlustbehaftete Daten. Zum Beispiel ist die Änderung eines `int32` zu einem `int64` eine kompatible Änderung, aber wenn ein Wert größer als INT32_MAX geschrieben wird, wird ein Client, der ihn als `int32` liest, die höherwertigen Bits verwerfen.

Sie können kompatible Änderungen an Ihrem Schema vornehmen, nur wenn Sie den Rollout in Ihrem System sorgfältig verwalten. Zum Beispiel können Sie einen `int32` in einen `int64` ändern, aber sicherstellen, dass Sie weiterhin nur gültige `int32`-Werte schreiben, bis das neue Schema auf allen Endpunkten bereitgestellt ist, und dann größere Werte danach schreiben.

Kompatibel, aber mit Problemen bei der Behandlung unbekannter Felder

Im Gegensatz zum binären Wire-Format propagieren ProtoJSON-Implementierungen im Allgemeinen keine unbekannten Felder. Das bedeutet, dass das Hinzufügen zu Schemata im Allgemeinen kompatibel ist, aber zu Parse-Fehlern führt, wenn ein Client mit dem alten Schema den neuen Inhalt beobachtet.

Das bedeutet, Sie können Ihrem Schema etwas hinzufügen, aber Sie können nicht sicher damit beginnen, es zu schreiben, bis Sie wissen, dass das Schema auf dem relevanten Client oder Server bereitgestellt wurde (oder dass die relevanten Clients ein "Ignore Unknown Fields"-Flag gesetzt haben, wie weiter unten besprochen).

  • Das Hinzufügen und Entfernen von Feldern gilt unter dieser Einschränkung als kompatibel.
  • Das Entfernen von Enum-Werten gilt unter dieser Einschränkung als kompatibel.

Kompatibel, aber potenziell verlustbehaftet

  • Das Ändern zwischen beliebigen der 32-Bit-Integer (`int32`, `uint32`, `sint32`, `sfixed32`, `fixed32`) und beliebigen der 64-Bit-Integer (`int64`, `uint64`, `sint64`, `sfixed32`) ist eine kompatible Änderung.
    • Wenn eine Zahl vom Wire gelesen wird, die nicht in den entsprechenden Typ passt, erhalten Sie den gleichen Effekt, als hätten Sie die Zahl in C++ in diesen Typ umgewandelt (zum Beispiel, wenn eine 64-Bit-Zahl als `int32` gelesen wird, wird sie auf 32 Bits gekürzt).
    • Im Gegensatz zum binären Wire-Format ist bool nicht mit Ganzzahlen kompatibel.
    • Beachten Sie, dass die `int64`-Typen standardmäßig in Anführungszeichen gesetzt werden, um Präzisionsverluste bei der Handhabung als `double` oder JavaScript-Zahl zu vermeiden, und die 32-Bit-Typen standardmäßig nicht in Anführungszeichen gesetzt sind. Konforme Implementierungen akzeptieren beide Fälle für alle Ganzzahltypen, aber nicht-konforme Implementierungen können diesen Fall falsch behandeln und keine mit Anführungszeichen versehenen `int32` oder nicht mit Anführungszeichen versehenen `int64` verarbeiten, was unter dieser Änderung zu Problemen führen kann.
  • enum kann bedingt mit string kompatibel sein
    • Wenn das Flag "enums-as-ints" von einem Client verwendet wird, sind Enums stattdessen mit Ganzzahltypen kompatibel.

Klarstellung zu RFC 3339

RFC 3339 zielt darauf ab, eine strenge Untermenge des ISO-8601-Formats zu deklarieren, und leider entstanden einige Mehrdeutigkeiten, da RFC 3339 im Jahr 2002 veröffentlicht wurde und ISO-8601 später überarbeitet wurde, ohne entsprechende Überarbeitungen von RFC 3339.

Insbesondere enthält ISO-8601-1988 diese Anmerkung:

In Datums- und Zeitdarstellungen dürfen Kleinbuchstaben verwendet werden, wenn Großbuchstaben nicht verfügbar sind.

Es ist unklar, ob diese Anmerkung vorschlägt, dass Parser Kleinbuchstaben im Allgemeinen akzeptieren sollten, oder ob sie nur vorschlägt, dass Kleinbuchstaben als Ersatz in Umgebungen verwendet werden können, in denen Großbuchstaben technisch nicht verwendet werden können. RFC 3339 enthält eine Anmerkung, die die Interpretation klären soll, dass Kleinbuchstaben im Allgemeinen akzeptiert werden sollten.

ISO-8601-2019 enthält die entsprechende Anmerkung nicht und ist eindeutig, dass Kleinbuchstaben nicht erlaubt sind. Dies hat zu einiger Verwirrung bei allen Bibliotheken geführt, die deklarieren, RFC 3339 zu unterstützen: Heute deklariert RFC 3339, dass es ein Profil von ISO-8601 ist, enthält aber eine Anmerkung, die sich auf etwas bezieht, das in der neuesten ISO-8601-Spezifikation nicht mehr vorhanden ist.

Die ProtoJSON-Spezifikation trifft die Entscheidung, dass das Zeitstempelformat die strengere Definition von "RFC 3339 als Profil von ISO-8601-2019" ist. Einige Protobuf-Implementierungen sind möglicherweise nicht konform, indem sie eine Zeitstempel-Parsing-Implementierung verwenden, die als "RFC 3339 als Profil von ISO-8601-1988" implementiert ist, was einige zusätzliche Randfälle akzeptiert.

Für konsistente Interoperabilität sollten Parser möglichst nur die strengere Teilmengenformat akzeptieren. Bei Verwendung einer nicht-konformen Implementierung, die die laxere Definition akzeptiert, vermeiden Sie es dringend, sich auf die zusätzlichen Randfälle zu verlassen, die akzeptiert werden.

JSON-Optionen

Eine konforme Protobuf JSON-Implementierung kann die folgenden Optionen bieten:

  • Felder ohne Präsenz immer ausgeben: Felder, die keine Präsenz unterstützen und ihren Standardwert haben, werden standardmäßig in der JSON-Ausgabe weggelassen (z. B. ein implizit präsenter Integer mit dem Wert 0, implizit präsente String-Felder, die leere Strings sind, und leere wiederholte Felder und Maps). Eine Implementierung kann eine Option anbieten, dieses Verhalten zu überschreiben und Felder mit ihren Standardwerten auszugeben.

    Ab Version v25.x sind die C++, Java und Python Implementierungen nicht konform, da dieses Flag proto2 optional Felder, aber nicht proto3 optional Felder beeinflusst. Eine Korrektur ist für eine zukünftige Version geplant.

  • Unbekannte Felder ignorieren: Der Protobuf JSON-Parser sollte unbekannte Felder standardmäßig ablehnen, kann aber eine Option bieten, unbekannte Felder beim Parsen zu ignorieren.

  • Proto-Feldname anstelle von lowerCamelCase verwenden: Standardmäßig konvertiert der Protobuf JSON-Drucker den Feldnamen in lowerCamelCase und verwendet diesen als JSON-Namen. Eine Implementierung kann eine Option anbieten, stattdessen den Proto-Feldnamen als JSON-Namen zu verwenden. Protobuf JSON-Parser müssen sowohl den konvertierten lowerCamelCase-Namen als auch den Proto-Feldnamen akzeptieren.

  • Enum-Werte als Ganzzahlen anstelle von Strings ausgeben: Der Name eines Enum-Werts wird standardmäßig in der JSON-Ausgabe verwendet. Eine Option kann bereitgestellt werden, um stattdessen den numerischen Wert des Enum-Werts zu verwenden.