Wire versus Protoc¶
Non-Primitive Types¶
Protoc generates literal equivalents for all the Proto3 new types like empty, struct, etc. Wire
tries to reuse existing types in the corresponding language when possible. The only new type Wire
brings is AnyMessage for the google.protobuf.Any proto type.
Any¶
The Any type wraps an arbitrary protobuf message by holding a field to identify its type and
another field for storing the serialized representation of the wrapped message. Wire comes with its
own AnyMessage type to represent google.protobuf.Any.
class AnyMessage(
val typeUrl: String,
val value: okio.ByteString
)
It comes with a few methods to wrap or unwrap the embedded message.
// Wire
val anyMessage: AnyMessage = AnyMessage.pack(person)
val person: Person = anyMessage.unpack(Person.ADAPTER)
// Protoc
val any: Any = Any.pack(foo)
val person: Person = any.unpack(Person.class)
Duration & Timestamp¶
Both google.protobuf.Duration and google.protobuf.Timestamp types will be generated by using
their JVM equivalent: java.time.Duration and java.time.Instant. For non-JVM platforms, we
provide two new Wire types with the same APIs:
class com.squareup.wire.Duration {
fun getSeconds(): Long
fun getNano(): Int
}
fun durationOfSeconds(seconds: Long, nano: Long): Duration
class com.squareup.wire.Instant {
fun getEpochSecond(): Long
fun getNano(): Int
}
fun ofEpochSecond(epochSecond: Long, nano: Long): Instant
// Wire
val duration: java.time.Duration = Duration.standardMinutes(15)
val instant: java.time.Instant = Instant.now()
// Protoc
val duration: google.protobuf.Duration =
Duration.newBuilder()
.setSeconds(60 * 15)
.build()
val instant: google.protobuf.Timestamp =
Timestamps.fromMillis(System.currentTimeMillis())
Struct¶
google.protobuf.Struct is meant mainly to represent JSON objects in code. Instead of building new
types, Wire reuses Java/Kotlin native types to represent all Struct types.
| Google Protobuf Type | Wire’s Java Equivalent | Wire’s Kotlin Equivalent |
|---|---|---|
Struct |
Map<String, ?> |
Map<String, ?>? |
ListValue |
List<?> |
List<?>? |
Value |
Object |
Any? |
NullValue |
Void |
Nothing? |
One difference worth noting between Protoc and Wire is that Protoc can make the difference between
an absent value, and a null value, Wire doesn’t. Wire will always write nulls in JSON objects
except at the root of it.
// Wire
val struct = mapOf("a" to 1.0)
val list = listOf(“b”, 2.0)
val boolValue = true
val nullValue = null
// Protoc
val struct: Struct =
Struct.newBuilder().apply {
putFields(“a”, Value.newBuilder.setNumberValue(1.0).build())
}
.build()
val list: List =
ListValue.newBuilder().apply {
addValues(Value.newBuilder.setStringValue(“a”).build())
addValues(Value.newBuilder.setNumberValue(2.0).build())
}
.build()
val boolValue = Value.newBuilder.setBoolValue(true).build()
val nullValue = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()
Wrappers¶
Wire didn’t create new types for wrappers either, each wrapper will be represented by a nullable
version of the primitive type it defines. For instance google.protobuf.FloatValue will be
represented in Java by the float boxed type @Nullable Float, in Kotlin by Float?.
// Wire
val floatValue = 33.3f
// Protoc
val floatValue = FloatValue.newBuilder().setValue(33.3f).build()
JSON¶
While Proto2 didn’t, Proto3 defines Protobuf serialization over JSON. Wire and Protoc are interoperable but their API are quite different. Wire offers JSON serialization over Moshi or Gson. Protoc brings its own JsonFormatter. Beware that Protoc throws an error for unknown fields, you need to configure it to opt-out of this behavior!
// Wire & Moshi
val moshi = Moshi.Builder()
.add(WireJsonAdapterFactory())
.build()
val adapter = moshi.adapter(Pizza::class.java)
val pizza: Pizza = ...
val json = adapter.toJson(pizza)
val parsedPizza = adapter.fromJson(json)
// Protoc
val pizza: PizzaOuterClass.Pizza = …
val json = JsonFormat.printer().print(value)
val jsonParser = JsonFormat.parser().ignoringUnknownFields()
val parsedBuilder = PizzaOuterClass.Pizza.newBuilder()
jsonParser.merge(json, parsedBuilder)
val parsedPizza = parsedBuilder.build()
Binary Field Order¶
Wire and Protoc can write the fields of a message in a different order.
Wire generated code writes fields in the declaration order of the .proto file.
It writes oneof members after all the other fields.
Protoc generated code, such as protobuf-java, writes fields in ascending tag order.
Both outputs are valid protobuf. Each library decodes the bytes of the other library to equal messages.
But equal messages can have different bytes. Do not use serialized bytes as a key, a hash, a signature, or a partition key across libraries. For example, the default Kafka partitioner hashes the serialized key. A Wire producer and a Protoc producer can send equal keys to different partitions (#3746).
Wire also does not guarantee identical bytes across its own releases. The 4.0.0 release notes say: “Identical encoding of equal messages across Wire releases is typical but not guaranteed”.
If you declare the fields of a message in tag order, Wire writes the plain fields in the same order as Protoc. This does not help when a oneof member has a lower tag than a plain field. In the message below, Protoc writes the tags in the order 1, 2, 3, and Wire writes them in the order 1, 3, 2.
message Key {
string a = 1;
oneof choice {
string b = 2;
}
string c = 3;
}
To write all fields in ascending tag order, set encodeInTagOrder = true on a kotlin {} or java {} target.
The Gradle plugin documentation describes this option.
Wire writes map entries in the iteration order of the map, as protobuf-java does by default.
protobuf-java can sort map entries with useDeterministicSerialization(), but Wire has no such option.
To get the same bytes for two equal maps, insert their entries in the same order, for example in sorted key order.