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()