AIP-2717
Patterns for generic fields
Developers have several options for how to represent generic values in proto messages. There are reasons to choose one over the other. Understanding them will lead to better and more consistent APIs.
Guidance
APIs should follow a consistent application of oneof
vs.
map<string, Foo>
vs. Any
vs. Struct
.
oneof
A oneof
is used to create a restriction on a set of optional fields,
enforcing that only one of them may be set (these fields are still separate
individual fields). A common pattern is to have a message that contains a
single oneof
collection of various message types. Such a oneof
message is
conceptually similar to a C union
, or C++ std::variant
. These should be
used in most places where a generic message type is needed, in preference to
other approaches.
Note: Adding additional possible values to an existing oneof
is a
non-breaking change, but moving existing fields into or out of a oneof
is
breaking (it creates a backwards-incompatible change in Go protobuf stubs).
map<string, Foo>
If a more generic structure is needed, a map of strings to objects may be
used. Such a map is represented by a normal JSON object, such as
{"a": "foo", "b": "bar"}
. The downside to such maps is that they are limited
to flat structures, and they can be difficult to work with because many string
constants may be needed.
Any
An Any
allows any message to be packed into the field. This is
conceptually similar to a "bytes" field containing a serialized message. The
advantage of an Any
is that a user-defined proto message can be stored along
with type information. The user must be able to know which kind of object is in
the Any
, which complicates the design. The disadvantage is that working with
and debugging any protos is much more complicated since the code may or may not
know how to deserialize the packed message. Also, the developer must contend
with unexpected types of messages in the Any
.
Any
should not be used as a request parameter. Request parameters will
either be a fixed set of types, in which case a oneof
should be used, or
a type descriptor would need to be sent along, in which case a struct
should be used, which achieves essentially the same thing with much simpler
semantics.
Struct
The Struct
message can be used to represent arbitrary nested JSON.
For example, given the following code:
Struct.Builder builder = Struct.newBuilder();
Value town = Value.newBuilder().setStringValue("Springfield").build();
Value population = Value.newBuilder().setNumberValue(273).build();
builder.putFields("town", town);
builder.putFields("population", population);
Struct survey = builder.build();
survey
would serialize as the actual JSON
{"town": "Springfield": "population": 273}
. This message type is fairly
uncommon, and should only be used rarely.