Skip to content

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.