追記
edition=2023 にできるならそちらがおすすめです。
protobuf editions=2023 について - だいたいよくわからないブログ
結論
背景
protobufでは値を省略したときに、その型で定められたデフォルト値が代入されます。
例えばstringを省略すると自動的に""
を指定したことになり、「値が指定されなかった」のか「空文字列を明示的に入れた」のかを区別しないように書くことが求められます。
この仕様は便利なのですが、時には区別したいケースもあります。 このとき下の方式2, 3のようなテクニックを用いて未指定とデフォルト値を区別することができます。
message Example { // 1. ダイレクトに定義(デフォルト値と未指定を区別できない) string foo = 1; // 2. 専用のmessageを定義 message Bar { string value = 1; } Bar bar = 2; // 3. oneofで定義 oneof baz_option { string baz = 3; } }
2のmessageをがんばって定義する形式は、パラメータが増えるとどんどんmessageが増えて辛くなってしまいます。
3は少しトリッキーなのがネックです。(また値の有無を switch-case
か if (getBazOptionCase == BAZ)
のように確認することになるため若干記述量も増します。)
wrappers.proto
と思ったらprimitive値をラップするものが公式で存在しました。
https://github.com/google/protobuf/blob/v3.1.0/src/google/protobuf/wrappers.proto#L31-L34
// Wrappers for primitive (non-message) types. These types are useful // for embedding primitives in the
google.protobuf.Any
type and for places // where we need to distinguish between the absence of a primitive // typed field and its default value.
とあり、まさに値がない事とデフォルト値を指定されたことを区別することができます。(Anyに値を突っ込むのにも使えるようです。)
これはmessageを都度定義する方式をただ汎用化したものかと思っていたのですが、仕様上で特別扱いされ、 Jsonにマッピングした際にラップを無視して単に1.0や"str"のようなプリミティブな値として表現されるようです。 https://github.com/google/protobuf/blob/v3.1.0/src/google/protobuf/wrappers.proto#L50
都度メッセージを定義してしまうと "nyan": { "value": "str" }
のような冗長なJsonになってしまうので、これを回避できるのは便利そうです。
仕様にもひっそりと Wrapper types
という表現で特別扱いされていました。
protocol buffers - JSON MAPPING
Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer.
実際に動かしてみると以下のようになります。(コードは https://github.com/matsu-chara/proto_wrap にあります)
syntax = "proto3"; package example; import "google/protobuf/wrappers.proto"; message Example { // 1. ダイレクトに定義(デフォルト値と未指定を区別できない) string foo = 1; // 2. 専用のmessageを定義 message Bar { string value = 1; } Bar bar = 2; // 3. oneofで定義 oneof baz_option { string baz = 3; } // 4. wrapperで定義 google.protobuf.StringValue mofu = 4; }
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.StringValue; import com.google.protobuf.util.JsonFormat; import example.Mofu.Example; public class Main { public static void main(String[] args) throws InvalidProtocolBufferException { Example ex = Example.newBuilder() .setFoo("foo") .setBar(Example.Bar.newBuilder().setValue("bar")) .setBaz("baz") .setMofu(StringValue.newBuilder().setValue("mofu")) .build(); System.out.println("instance.toString"); System.out.println(ex.toString()); System.out.println("json"); System.out.println(JsonFormat.printer().print(ex)); System.out.printf(""); Example empty = Example.getDefaultInstance(); // foo == "" System.out.println("foo isEmpty: " + empty.getFoo().isEmpty()); // bar == null (getBar returns defaultBar if bar == null) System.out.println("hasBar: " + empty.hasBar()); System.out.println("bar isEmpty: " + empty.getBar().getValue().isEmpty()); // baz == BAZOPTION_NOT_SET (getBaz returns "" if BazOptionCase != Baz) System.out.println("bazOptionCase: " + empty.getBazOptionCase()); System.out.println("baz isEmpty: " + empty.getBaz().isEmpty()); // mofu == null (getMofu returns defaultMofu if mofu == null) System.out.println("mofu hasMofu: " + empty.hasMofu()); System.out.println("mofu isEmpty: " + empty.getMofu().getValue().isEmpty()); } }
instance.toString instance.toString foo: "foo" bar { value: "bar" } baz: "baz" mofu { value: "mofu" } json { "foo": "foo", "bar": { "value": "bar" }, "baz": "baz", "mofu": "mofu" } foo isEmpty: true hasBar: false bar isEmpty: true bazOptionCase: BAZOPTION_NOT_SET baz isEmpty: true mofu hasMofu: false mofu isEmpty: true
jsonの出力ではStringWrapperにあたる部分が簡略化されていることが分かります。
emptyの取得ではhasMofuやoneofといったテクニックで値が無いことが確認できます。
このようにWrapper types
を使えば未指定とデフォルト値を区別することが簡単にできて、なおかつjson変換の際に冗長さを減らすことができます。
もちろんフィールドが増えたり使いまわされたりするならmessageを定義するのが良いこともあるので、適切に使い分ける必要があるとは思いますが、単純にwrapしたいだけであれば選択肢に入ると思います。
ただし不用意にラップすると(クラスが増えるので)ビルド時間が増えたりするといったデメリットがあったりしますし、そもそものprotobufの設計思想から若干それているような気がしなくもないので第一選択肢としてはデフォルト値を利用する戦略を取るのが望ましい気がします。