ProtoJSON 形式

ProtobufからJSONへの変換ユーティリティの使用方法を説明します。

ProtobufはJSONでの正規エンコーディングをサポートしており、標準のProtobufバイナリワイヤー形式をサポートしていないシステムとのデータ共有を容易にします。

このページでは形式を規定しますが、準拠したProtoJSONパーサーを定義するいくつかの追加のエッジケースは、Protobuf Conformance Test Suiteでカバーされており、ここでは網羅的に詳述されていません。

この形式の非目標

一部のJSONスキーマを表現できない

ProtoJSON形式は、Protobufスキーマ言語で表現可能なスキーマのJSON表現となるように設計されています。

多くの既存のJSONスキーマをProtobufスキーマとして表現し、ProtoJSONを使用して解析することは可能かもしれませんが、任意のJSONスキーマを表現できるように設計されているわけではありません。

例えば、number[][]number|stringのようなJSONスキーマで一般的な型をProtobufスキーマで表現する方法はありません。

google.protobuf.Structgoogle.protobuf.Value型を使用することで、任意のJSONをProtobufスキーマに解析できますが、これらは値をスキーマレスで順序のないキーと値のマップとしてキャプチャすることしかできません。

バイナリワイヤー形式ほど効率的ではない

ProtoJSON形式は、バイナリワイヤー形式ほど効率的ではなく、将来もそうなることはありません。

コンバーターはメッセージのエンコードとデコードにより多くのCPUを使用し、(稀なケースを除き)エンコードされたメッセージはより多くのスペースを消費します。

バイナリワイヤー形式ほど優れたスキーマ進化の保証がない

ProtoJSON形式は未知のフィールドをサポートしておらず、フィールド名やenum値の名前をエンコードされたメッセージに含めるため、後でそれらの名前を変更することが非常に難しくなります。フィールドの削除は、解析エラーを引き起こす破壊的変更です。

詳細については、下記のJSONワイヤーセーフティを参照してください。

形式の説明

各型の表現

以下の表は、データがJSONファイルでどのように表現されるかを示しています。

ProtobufJSONJSONの例注釈
messageobject{"fooBar": v, "g": null, ...}JSONオブジェクトを生成します。メッセージのフィールド名はlowerCamelCaseにマッピングされ、JSONオブジェクトのキーになります。json_nameフィールドオプションが指定されている場合は、指定された値が代わりにキーとして使用されます。パーサーは、lowerCamelCase名(またはjson_nameオプションで指定された名前)と元のprotoフィールド名の両方を受け入れます。nullはすべてのフィールド型で受け入れられる値であり、フィールドは未設定のままになります。\0 (nul)json_nameの値の中では使用できません。理由の詳細については、json_nameのより厳格な検証を参照してください。
enumstring"FOO_BAR"protoで指定されたenum値の名前が使用されます。パーサーは、enum名と整数値の両方を受け入れます。
map<K,V>object{"k": v, ...}すべてのキーは文字列に変換されます(JSON仕様ではキーは文字列のみです)。
repeated Varray[v, ...]nullは空のリスト[]として受け入れられます。
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON値は、パディング付きの標準base64エンコーディングを使用して文字列としてエンコードされたデータになります。パディングの有無にかかわらず、標準またはURLセーフなbase64エンコーディングが受け入れられます。
int32, fixed32, uint32number1, -10, 0JSON値は10進数になります。数値または文字列のどちらも受け入れられます。空の文字列は無効です。指数表記(`1e2`など)は、引用符付きと引用符なしの両方の形式で受け入れられます。
int64, fixed64, uint64string"1", "-10"JSON値は10進数の文字列になります。数値または文字列のどちらも受け入れられます。空の文字列は無効です。指数表記(`1e2`など)は、引用符付きと引用符なしの両方の形式で受け入れられます。
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON値は数値、または特別な文字列値 "NaN"、"Infinity"、"-Infinity" のいずれかになります。数値または文字列のどちらも受け入れられます。空の文字列は無効です。指数表記も受け入れられます。
Anyobject{"@type": "url", "f": v, ... }Anyに特別なJSONマッピングを持つ値が含まれている場合、次のように変換されます: {"@type": xxx, "value": yyy}。それ以外の場合、値はJSONオブジェクトに変換され、実際のデータ型を示すために"@type"フィールドが挿入されます。
Timestampstring"1972-01-01T10:00:20.021Z"RFC 3339を使用します(明確化を参照)。生成される出力は常にZ正規化され、0、3、6、または9桁の小数部を使用します。"Z"以外のオフセットも受け入れられます。
Durationstring"1.000340012s", "1s"生成される出力には、必要な精度に応じて常に0、3、6、または9桁の小数部が含まれ、その後に接尾辞 "s" が続きます。ナノ秒の精度に収まる限り、任意の小数桁(なしも可)が受け入れられ、接尾辞 "s" が必要です。
Structobject{ ... }任意のJSONオブジェクト。struct.protoを参照してください。
ラッパー型さまざまな型2, "2", "foo", true, "true", null, 0, ...ラッパーは、nullが許可され、データ変換および転送中に保持される点を除き、ラップされたプリミティブ型と同じJSON表現を使用します。
FieldMaskstring"f.fooBar,h"field_mask.protoを参照してください。
ListValuearray[foo, bar, ...]
Valuevalue任意のJSON値。詳細についてはgoogle.protobuf.Valueを確認してください。
NullValuenullJSONのnull。[nullの解析動作](#null-values)の特殊なケースです。
Emptyobject{}空のJSONオブジェクト

存在確認とデフォルト値

プロトコルバッファからJSONエンコードされた出力を生成する場合、フィールドが存在確認をサポートしているなら、シリアライザは対応するhasserがtrueを返す場合にのみフィールド値を出力しなければなりません。

フィールドが存在確認をサポートしておらず、デフォルト値を持つ場合(例えば、空のrepeatedフィールドなど)、シリアライザはそれを​​出力から省略するべきです。実装によっては、デフォルト値を持つフィールドを出力に含めるオプションを提供する場合があります。

null値

シリアライザはnull値を出力するべきではありません。

パーサーは、任意のフィールドに対してnullを正当な値として受け入れるべきです。その際の動作は以下の通りです。

  • キーの有効性チェックは引き続き行われるべきです(未知のフィールドは許可しない)。
  • フィールドは、入力に全く存在しなかったかのように未設定のままであるべきです(該当する場合、hasserは引き続きfalseを返す必要があります)。

null値はrepeatedフィールド内では許可されません。

google.protobuf.NullValueは、この動作の特別な例外です。nullはこの型に対してセンチネル(番兵)的な存在値として扱われるため、この型のフィールドはシリアライザとパーサーによって標準の存在確認動作の下で処理されなければなりません。この動作により、google.protobuf.Structgoogle.protobuf.Valueは任意のJSONを損失なくラウンドトリップさせることができます。

重複した値

シリアライザは、同じフィールドを複数回シリアライズしたり、同じoneof内の複数の異なるcaseを同じJSONオブジェクト内でシリアライズしたりしてはなりません。

パーサーは、同じフィールドが重複していることを受け入れ、最後に提供された値を保持するべきです。これは、同じフィールド名の「別のスペル」にも適用されます。

実装がフィールドの順序に関する必要な情報を維持できない場合、任意の値が勝つよりも、重複したキーを持つ入力を拒否することが望ましいです。一部の実装では、オブジェクトのフィールド順序を維持することが非現実的または不可能な場合があるため、可能な限りProtoJSONの重複フィールドに関する特定の動作に依存しないシステムにすることを強くお勧めします。

範囲外の数値

数値を解析する際に、ワイヤーから解析された数値が対応する型に収まらない場合、パーサーはその値を適切な型に強制変換するべきです。これは、C++やJavaでの単純なキャストと同じ動作をします(例えば、2^32より大きい数がint32フィールドとして読み取られた場合、32ビットに切り捨てられます)。

ProtoJSONワイヤーセーフティ

ProtoJSONを使用する場合、分散システムで安全に行えるスキーマ変更は一部のみです。これは、バイナリワイヤー形式に適用される同じ概念とは対照的です。

JSONワイヤーアンセーフな変更

ワイヤーアンセーフな変更とは、古いスキーマを使用してシリアライズされたデータを新しいスキーマを使用するパーサーで解析した場合(またはその逆)に壊れるスキーマ変更です。この種のスキーマ変更はほとんど行うべきではありません。

  • フィールドを同じ番号と型の拡張との間で変更することは安全ではありません。
  • フィールドをstringbytesの間で変更することは安全ではありません。
  • フィールドをメッセージ型とbytesの間で変更することは安全ではありません。
  • 任意のフィールドをoptionalからrepeatedに変更することは安全ではありません。
  • フィールドをmap<K, V>と対応するrepeatedメッセージフィールドの間で変更することは安全ではありません。
  • フィールドを既存の`oneof`に移動することは安全ではありません。

JSONワイヤーセーフな変更

ワイヤーセーフな変更とは、データの損失や新たなパース失敗のリスクなしに、スキーマをこの方法で進化させることが完全に安全な変更です。

ほぼすべてのワイヤーセーフな変更は、アプリケーションコードにとって破壊的変更になる可能性があることに注意してください。例えば、既存のenumに値を追加すると、そのenumに対する網羅的なswitchを持つコードのコンパイルが壊れます。そのため、Googleは公開メッセージに対してこれらの種類の変更を避けることがあります。AIPには、そこで安全に行える変更に関するガイダンスが含まれています。

  • 単一のoptionalフィールドを新しいoneofのメンバーに変更することは安全です。
  • フィールドを1つしか含まないoneofoptionalフィールドに変更することは安全です。
  • フィールドをint32sint32sfixed32fixed32の間で変更することは安全です。
  • フィールドをint64sint64sfixed64fixed64の間で変更することは安全です。
  • フィールド番号を変更することは安全です(ProtoJSON形式ではフィールド番号が使用されないため)。ただし、バイナリワイヤー形式では非常に安全ではないため、依然として強く非推奨です。
  • enumに値を追加することは、すべての関連クライアントで「Enum値を整数として出力する」が設定されている場合に安全です(オプションを参照)。

JSONワイヤー互換の変更(条件付きで安全)

ワイヤーセーフな変更とは異なり、ワイヤー互換とは、特定の変更の前後で同じデータを解析できることを意味します。ただし、それを読み取るクライアントは、この種の変更の下で損失のあるデータを取得します。例えば、int32をint64に変更することは互換性のある変更ですが、INT32_MAXより大きい値が書き込まれた場合、それをint32として読み取るクライアントは上位ビットを破棄します。

システムのロールアウトを慎重に管理する場合にのみ、スキーマに互換性のある変更を加えることができます。例えば、int32をint64に変更しても、新しいスキーマがすべてのエンドポイントにデプロイされるまで正当なint32値のみを書き込み続け、その後でより大きな値の書き込みを開始することができます。

未知のフィールドの処理に問題がある互換性

バイナリワイヤー形式とは異なり、ProtoJSON実装は通常、未知のフィールドを伝播しません。これは、スキーマへの追加は一般的に互換性がありますが、古いスキーマを使用するクライアントが新しいコンテンツを観測した場合に解析失敗につながることを意味します。

これは、スキーマに追加することはできますが、そのスキーマが関連するクライアントまたはサーバーにデプロイされたこと(または関連するクライアントが「未知のフィールドを無視する」フラグを設定したこと、下記参照)を確認するまで、安全にそれらを書き始めることはできないことを意味します。

  • フィールドの追加と削除は、この注意点を考慮すれば互換性があると見なされます。
  • enum値の削除は、この注意点を考慮すれば互換性があると見なされます。

損失の可能性がある互換性

  • 32ビット整数(int32uint32sint32sfixed32fixed32)と64ビット整数(int64uint64sint64sfixed64)の間での変更は、互換性のある変更です。
    • ワイヤーから解析された数値が対応する型に収まらない場合、C++でその数値をその型にキャストした場合と同じ効果が得られます(例えば、64ビットの数値がint32として読み取られた場合、32ビットに切り捨てられます)。
    • バイナリワイヤー形式とは異なり、boolは整数と互換性がありません。
    • int64型は、doubleやJavaScriptのnumberとして扱われる際の精度損失を避けるためにデフォルトで引用符で囲まれ、32ビット型はデフォルトで引用符で囲まれないことに注意してください。準拠した実装はすべての整数型でどちらのケースも受け入れますが、非準拠の実装はこのケースを誤って処理し、引用符付きのint32や引用符なしのint64を扱えない可能性があり、この変更で壊れる可能性があります。
  • enumstringと条件付きで互換性がある場合があります。
    • いずれかのクライアントで「enums-as-ints」フラグが使用されている場合、enumは代わりに整数型と互換性があります。

RFC 3339の明確化

RFC 3339は、ISO-8601形式の厳密なサブセットを宣言することを意図していますが、残念ながらRFC 3339が2002年に公開され、その後ISO-8601がRFC 3339の対応する改訂なしに改訂されたため、いくつかの曖昧さが生まれました。

最も注目すべきは、ISO-8601-1988に次の注記が含まれていることです。

日付と時刻の表現において、大文字が利用できない場合は小文字を使用してもよい。

この注記が、パーサーが一般的に小文字を受け入れるべきだと示唆しているのか、それとも技術的に大文字が使用できない環境での代替として小文字を使用してもよいと示唆しているだけなのかは曖昧です。RFC 3339には、一般的に小文字を受け入れるべきだという解釈を明確にすることを意図した注記が含まれています。

ISO-8601-2019には対応する注記が含まれておらず、小文字が許可されていないことは明白です。これにより、RFC 3339をサポートすると宣言するすべてのライブラリに混乱が生じました。現在、RFC 3339はISO-8601のプロファイルであると宣言していますが、最新のISO-8601仕様にはもはや存在しないものに関する注記が含まれています。

ProtoJSON仕様は、タイムスタンプ形式を「ISO-8601-2019のプロファイルとしてのRFC 3339」のより厳密な定義とする決定を下しています。一部のProtobuf実装は、「ISO-8601-1988のプロファイルとしてのRFC 3339」として実装されたタイムスタンプ解析実装を使用することで非準拠である可能性があり、その場合はいくつかの追加のエッジケースを受け入れます。

一貫した相互運用性のために、パーサーは可能な限り厳密なサブセット形式のみを受け入れるべきです。より緩やかな定義を受け入れる非準拠の実装を使用する場合、追加のエッジケースが受け入れられることに依存することは強く避けてください。

JSONオプション

準拠したprotobuf JSON実装は、以下のオプションを提供する場合があります。

  • 存在確認のないフィールドを常に出力する: 存在確認をサポートせず、デフォルト値を持つフィールドは、デフォルトでJSON出力から省略されます(例えば、値が0の暗黙的な存在確認の整数、空文字列の暗黙的な存在確認の文字列フィールド、空のrepeatedおよびmapフィールド)。実装は、この動作を上書きしてデフォルト値を持つフィールドを出力するオプションを提供する場合があります。

    v25.xの時点では、C++、Java、およびPythonの実装は非準拠です。なぜなら、このフラグはproto2のoptionalフィールドには影響しますが、proto3のoptionalフィールドには影響しないためです。将来のリリースで修正が計画されています。

  • 未知のフィールドを無視する: protobuf JSONパーサーは、デフォルトで未知のフィールドを拒否するべきですが、解析時に未知のフィールドを無視するオプションを提供する場合があります。

  • lowerCamelCase名の代わりにprotoフィールド名を使用する: デフォルトでは、protobuf JSONプリンターはフィールド名をlowerCamelCaseに変換し、それをJSON名として使用します。実装は、代わりにprotoフィールド名をJSON名として使用するオプションを提供する場合があります。protobuf JSONパーサーは、変換されたlowerCamelCase名とprotoフィールド名の両方を受け入れる必要があります。

  • enum値を文字列の代わりに整数として出力する: デフォルトでは、enum値の名前がJSON出力で使用されます。代わりにenum値の数値を使用するオプションが提供される場合があります。