Protocol BuffersをKotlinで出力する（Wire 3.0.x）

## この記事の内容
Square社のProtocol Buffers実装であるWireが、3.0.0でKotlinに対応しました。
これまでKotlinメインのプロジェクトでもproto生成ファイルだけはJavaコードを使用することが多かったと思いますが（実際、`Kotlin protobuf`等で検索しても、出てくる記事の多くはJavaを使っていました）、Kotlin化できるかもー！　ということで使ってみました。
この記事では、執筆時点での最新版 [Wire 3.0.1](https://github.com/square/wire/releases/tag/wire-3.0.1) を使用しています。現在も絶賛開発中なので、導入される場合は本家サイトの状況をご確認ください。

## Kotlinファイルの生成

公式では [wire-gradle-plugin](https://github.com/square/wire/blob/wire-3.0.1/docs/wire_compiler.md) での導入が紹介されていますが、執筆時点では `plugins { id 'com.squareup.wire' }` では参照できず `classpath` + `apply plugin` 形式で記述することでビルドできました（[gradle plugin検索](https://plugins.gradle.org)でもヒットしなかったので、まだ登録されていないようです）。
また、gradle pluginではproto3の変換はできず、proto3を変換しようとすると以下のメッセージが出てスキップされます。

> Skipped .proto files with unsupported syntax. Add this line to fix:
>   syntax = "proto2";

proto3対応自体がin progressのようなので、ここは今後に期待です。

この記事では、以前のバージョンと同じように、コマンドラインでjarを実行する方法でコードを生成していきます。
コマンド実行時の違いは、 `--java_out`オプションを`--kotlin_out`に変更するだけです。`--java_out`と`--kotlin_out`を同時に指定することはできません。

```bash
$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \
     --proto_path=src/main/proto \
     --kotlin_out=src/main/kotlin
```

## 生成されたJavaコードとKotlinコードの違い

生成されたJavaとKotlinのコードを比較するため、以下の簡単なprotoファイルからそれぞれのファイルを生成してみます。

```proto:proto2_sample.proto
syntax = "proto2";

package com.github.kazukinr.sample;

option java_package = "com.github.kazukinr.sample.proto";

message Proto2Sample {
    required int64 id = 1;
    optional string name = 2;
    repeated string roles = 3;
}
```

### Java
Android用の`--android`オプション（`Parcelable`が実装されます）、および3.0.0で追加された`--android-annotations`オプションをつけて生成した結果を記載します。

```bash
$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \
     --proto_path=src/main/proto \
     --java_out=src/main/java \
     --android \
     --android-annotations
```

#### 出力結果

```java:Proto2Sample.java
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: proto2_sample.proto
package com.github.kazukinr.sample.proto;

import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.squareup.wire.AndroidMessage;
import com.squareup.wire.FieldEncoding;
import com.squareup.wire.Message;
import com.squareup.wire.ProtoAdapter;
import com.squareup.wire.ProtoReader;
import com.squareup.wire.ProtoWriter;
import com.squareup.wire.WireField;
import com.squareup.wire.internal.Internal;
import java.io.IOException;
import java.lang.Long;
import java.lang.Object;
import java.lang.Override;
import java.lang.String;
import java.lang.StringBuilder;
import java.util.List;
import okio.ByteString;

public final class Proto2Sample extends AndroidMessage<Proto2Sample, Proto2Sample.Builder> {
  public static final ProtoAdapter<Proto2Sample> ADAPTER = new ProtoAdapter_Proto2Sample();

  public static final Parcelable.Creator<Proto2Sample> CREATOR = AndroidMessage.newCreator(ADAPTER);

  private static final long serialVersionUID = 0L;

  public static final Long DEFAULT_ID = 0L;

  public static final String DEFAULT_NAME = "";

  @WireField(
      tag = 1,
      adapter = "com.squareup.wire.ProtoAdapter#INT64",
      label = WireField.Label.REQUIRED
  )
  public final Long id;

  @WireField(
      tag = 2,
      adapter = "com.squareup.wire.ProtoAdapter#STRING"
  )
  @Nullable
  public final String name;

  @WireField(
      tag = 3,
      adapter = "com.squareup.wire.ProtoAdapter#STRING",
      label = WireField.Label.REPEATED
  )
  public final List<String> roles;

  public Proto2Sample(Long id, @Nullable String name, List<String> roles) {
    this(id, name, roles, ByteString.EMPTY);
  }

  public Proto2Sample(Long id, @Nullable String name, List<String> roles,
      ByteString unknownFields) {
    super(ADAPTER, unknownFields);
    this.id = id;
    this.name = name;
    this.roles = Internal.immutableCopyOf("roles", roles);
  }

  @Override
  public Builder newBuilder() {
    Builder builder = new Builder();
    builder.id = id;
    builder.name = name;
    builder.roles = Internal.copyOf(roles);
    builder.addUnknownFields(unknownFields());
    return builder;
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof Proto2Sample)) return false;
    Proto2Sample o = (Proto2Sample) other;
    return unknownFields().equals(o.unknownFields())
        && id.equals(o.id)
        && Internal.equals(name, o.name)
        && roles.equals(o.roles);
  }

  @Override
  public int hashCode() {
    int result = super.hashCode;
    if (result == 0) {
      result = unknownFields().hashCode();
      result = result * 37 + id.hashCode();
      result = result * 37 + (name != null ? name.hashCode() : 0);
      result = result * 37 + roles.hashCode();
      super.hashCode = result;
    }
    return result;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append(", id=").append(id);
    if (name != null) builder.append(", name=").append(name);
    if (!roles.isEmpty()) builder.append(", roles=").append(roles);
    return builder.replace(0, 2, "Proto2Sample{").append('}').toString();
  }

  public static final class Builder extends Message.Builder<Proto2Sample, Builder> {
    public Long id;

    public String name;

    public List<String> roles;

    public Builder() {
      roles = Internal.newMutableList();
    }

    public Builder id(Long id) {
      this.id = id;
      return this;
    }

    public Builder name(String name) {
      this.name = name;
      return this;
    }

    public Builder roles(List<String> roles) {
      Internal.checkElementsNotNull(roles);
      this.roles = roles;
      return this;
    }

    @Override
    public Proto2Sample build() {
      if (id == null) {
        throw Internal.missingRequiredFields(id, "id");
      }
      return new Proto2Sample(id, name, roles, super.buildUnknownFields());
    }
  }

  private static final class ProtoAdapter_Proto2Sample extends ProtoAdapter<Proto2Sample> {
    public ProtoAdapter_Proto2Sample() {
      super(FieldEncoding.LENGTH_DELIMITED, Proto2Sample.class);
    }

    @Override
    public int encodedSize(Proto2Sample value) {
      return ProtoAdapter.INT64.encodedSizeWithTag(1, value.id)
          + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name)
          + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles)
          + value.unknownFields().size();
    }

    @Override
    public void encode(ProtoWriter writer, Proto2Sample value) throws IOException {
      ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id);
      ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name);
      ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles);
      writer.writeBytes(value.unknownFields());
    }

    @Override
    public Proto2Sample decode(ProtoReader reader) throws IOException {
      Builder builder = new Builder();
      long token = reader.beginMessage();
      for (int tag; (tag = reader.nextTag()) != -1;) {
        switch (tag) {
          case 1: builder.id(ProtoAdapter.INT64.decode(reader)); break;
          case 2: builder.name(ProtoAdapter.STRING.decode(reader)); break;
          case 3: builder.roles.add(ProtoAdapter.STRING.decode(reader)); break;
          default: {
            reader.readUnknownField(tag);
          }
        }
      }
      builder.addUnknownFields(reader.endMessageAndGetUnknownFields(token));
      return builder.build();
    }

    @Override
    public Proto2Sample redact(Proto2Sample value) {
      Builder builder = value.newBuilder();
      builder.clearUnknownFields();
      return builder.build();
    }
  }
}
```

`optional`を定義したnameフィールドに`@Nullable`アノテーションが指定されているのがわかります。


### Kotlin
こちらも`--android`オプションを指定して生成した結果を記載します。Kotlinの場合、`--android-annotations`オプションの有無による生成ファイルの違いはありませんでした。

```bash
$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \
     --proto_path=src/main/proto \
     --java_out=src/main/java \
     --android
```

#### 出力結果

```kotlin:Proto2Sample.kt
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: proto2_sample.proto
package com.github.kazukinr.sample.proto

import android.os.Parcelable
import com.squareup.wire.AndroidMessage
import com.squareup.wire.FieldEncoding
import com.squareup.wire.ProtoAdapter
import com.squareup.wire.ProtoReader
import com.squareup.wire.ProtoWriter
import com.squareup.wire.WireField
import com.squareup.wire.internal.missingRequiredFields
import kotlin.Any
import kotlin.AssertionError
import kotlin.Boolean
import kotlin.Deprecated
import kotlin.DeprecationLevel
import kotlin.Int
import kotlin.Long
import kotlin.Nothing
import kotlin.String
import kotlin.collections.List
import kotlin.hashCode
import kotlin.jvm.JvmField
import okio.ByteString

class Proto2Sample(
  @field:WireField(
    tag = 1,
    adapter = "com.squareup.wire.ProtoAdapter#INT64",
    label = WireField.Label.REQUIRED
  )
  val id: Long,
  @field:WireField(
    tag = 2,
    adapter = "com.squareup.wire.ProtoAdapter#STRING"
  )
  val name: String? = null,
  @field:WireField(
    tag = 3,
    adapter = "com.squareup.wire.ProtoAdapter#STRING",
    label = WireField.Label.REPEATED
  )
  val roles: List<String> = emptyList(),
  unknownFields: ByteString = ByteString.EMPTY
) : AndroidMessage<Proto2Sample, Nothing>(ADAPTER, unknownFields) {
  @Deprecated(
    message = "Shouldn't be used in Kotlin",
    level = DeprecationLevel.HIDDEN
  )
  override fun newBuilder(): Nothing = throw AssertionError()

  override fun equals(other: Any?): Boolean {
    if (other === this) return true
    if (other !is Proto2Sample) return false
    return unknownFields == other.unknownFields
        && id == other.id
        && name == other.name
        && roles == other.roles
  }

  override fun hashCode(): Int {
    var result = super.hashCode
    if (result == 0) {
      result = unknownFields.hashCode()
      result = result * 37 + id.hashCode()
      result = result * 37 + name.hashCode()
      result = result * 37 + roles.hashCode()
      super.hashCode = result
    }
    return result
  }

  override fun toString(): String {
    val result = mutableListOf<String>()
    result += """id=$id"""
    if (name != null) result += """name=$name"""
    if (roles.isNotEmpty()) result += """roles=$roles"""
    return result.joinToString(prefix = "Proto2Sample{", separator = ", ", postfix = "}")
  }

  fun copy(
    id: Long = this.id,
    name: String? = this.name,
    roles: List<String> = this.roles,
    unknownFields: ByteString = this.unknownFields
  ): Proto2Sample = Proto2Sample(id, name, roles, unknownFields)

  companion object {
    @JvmField
    val ADAPTER: ProtoAdapter<Proto2Sample> = object : ProtoAdapter<Proto2Sample>(
      FieldEncoding.LENGTH_DELIMITED, 
      Proto2Sample::class
    ) {
      override fun encodedSize(value: Proto2Sample): Int = 
        ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) +
        ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) +
        ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) +
        value.unknownFields.size

      override fun encode(writer: ProtoWriter, value: Proto2Sample) {
        ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id)
        ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name)
        ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles)
        writer.writeBytes(value.unknownFields)
      }

      override fun decode(reader: ProtoReader): Proto2Sample {
        var id: Long? = null
        var name: String? = null
        val roles = mutableListOf<String>()
        val unknownFields = reader.forEachTag { tag ->
          when (tag) {
            1 -> id = ProtoAdapter.INT64.decode(reader)
            2 -> name = ProtoAdapter.STRING.decode(reader)
            3 -> roles.add(ProtoAdapter.STRING.decode(reader))
            else -> reader.readUnknownField(tag)
          }
        }
        return Proto2Sample(
          id = id ?: throw missingRequiredFields(id, "id"),
          name = name,
          roles = roles,
          unknownFields = unknownFields
        )
      }

      override fun redact(value: Proto2Sample): Proto2Sample = value.copy(
        unknownFields = ByteString.EMPTY
      )
    }

    @JvmField
    val CREATOR: Parcelable.Creator<Proto2Sample> = AndroidMessage.newCreator(ADAPTER)
  }
}
```

`required`、`optional`の設定がKotlinでも同様に定義されています。
また、optional propertyに関してはコンストラクタでデフォルト引数としてnullが設定されています。
`newBuilder()`は例外を投げるようになり、かわりに`copy`関数が定義されています。


### 生成ファイル使用時の違い

#### protoの参照
参照に関しては、JavaとKotlinで大きな違いはありません。Javaコードを生成した場合もKotlinからはプロパティ参照していると思うので、Java参照時にきちんとnullをケアしている場合は、Kotlin変更時にコードを修正する必要はなさそうです。nullチェックがコンパイル時に走るため（これまで`@Nullable`がついてないのであれば）、より安全にコードを書くことができます。

#### protoの生成
JavaコードではBuilderパターンが使用されていましたが、KotlinではBuilderは生成されず、proto生成時にはコンストラクタに必要な項目を名前付き引数で与えることになります。optional propertyにはデフォルト値が設定されているので、必要な項目だけ指定すれば大丈夫です。

```kotlin
// Proto2Sample.javaを使用
val proto = Proto2Sample.Builder()
                .id("id")
                .name("name")
                .roles(listOf("user", "staff"))
                .build()

// Proto2Sample.ktを使用
val proto = Proto2Sample(
                id = "id",
                name = "name",
                roles = listOf("user", "staff")
            )
```

#### protoのコピー・一部更新
生成と同様にprotoのコピーや一部propertyの更新も書き方が変わります。

```kotlin
// Proto2Sample.javaを使用
val proto2 = proto.newBuilder()
                .name("name2")
                .build()

// Proto2Sample.ktを使用
val proto2 = proto.copy(name = "name2")
```

`copy`が定義されているので、`data class`のような使い方ができます。


## proto3 syntax

proto3形式についても同様に出力した結果を記載します。前述しましたが、proto3対応はまだ完全ではないようで、一部期待通りの出力になっていない部分もありました。

元ネタのprotoファイルです。

```proto:proto3_sample.proto
syntax = "proto3";

package com.github.kazukinr.sample;

option java_package = "com.github.kazukinr.sample.proto";

message Proto3Sample {
    int64 id = 1;
    string name = 2;
    repeated string roles = 3;
}
```

proto3では`required`、`optional`は廃止されており、すべて`optional`扱いとなります。

### Java

```java:Proto3Sample.java
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: proto3_sample.proto
package com.github.kazukinr.sample.proto;

import android.os.Parcelable;
import com.squareup.wire.AndroidMessage;
import com.squareup.wire.FieldEncoding;
import com.squareup.wire.Message;
import com.squareup.wire.ProtoAdapter;
import com.squareup.wire.ProtoReader;
import com.squareup.wire.ProtoWriter;
import com.squareup.wire.WireField;
import com.squareup.wire.internal.Internal;
import java.io.IOException;
import java.lang.Long;
import java.lang.Object;
import java.lang.Override;
import java.lang.String;
import java.lang.StringBuilder;
import java.util.List;
import okio.ByteString;

public final class Proto3Sample extends AndroidMessage<Proto3Sample, Proto3Sample.Builder> {
  public static final ProtoAdapter<Proto3Sample> ADAPTER = new ProtoAdapter_Proto3Sample();

  public static final Parcelable.Creator<Proto3Sample> CREATOR = AndroidMessage.newCreator(ADAPTER);

  private static final long serialVersionUID = 0L;

  public static final Long DEFAULT_ID = 0L;

  public static final String DEFAULT_NAME = "";

  @WireField(
      tag = 1,
      adapter = "com.squareup.wire.ProtoAdapter#INT64"
  )
  public final Long id;

  @WireField(
      tag = 2,
      adapter = "com.squareup.wire.ProtoAdapter#STRING"
  )
  public final String name;

  @WireField(
      tag = 3,
      adapter = "com.squareup.wire.ProtoAdapter#STRING",
      label = WireField.Label.REPEATED
  )
  public final List<String> roles;

  public Proto3Sample(Long id, String name, List<String> roles) {
    this(id, name, roles, ByteString.EMPTY);
  }

  public Proto3Sample(Long id, String name, List<String> roles, ByteString unknownFields) {
    super(ADAPTER, unknownFields);
    this.id = id;
    this.name = name;
    this.roles = Internal.immutableCopyOf("roles", roles);
  }

  @Override
  public Builder newBuilder() {
    Builder builder = new Builder();
    builder.id = id;
    builder.name = name;
    builder.roles = Internal.copyOf(roles);
    builder.addUnknownFields(unknownFields());
    return builder;
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof Proto3Sample)) return false;
    Proto3Sample o = (Proto3Sample) other;
    return unknownFields().equals(o.unknownFields())
        && Internal.equals(id, o.id)
        && Internal.equals(name, o.name)
        && roles.equals(o.roles);
  }

  @Override
  public int hashCode() {
    int result = super.hashCode;
    if (result == 0) {
      result = unknownFields().hashCode();
      result = result * 37 + (id != null ? id.hashCode() : 0);
      result = result * 37 + (name != null ? name.hashCode() : 0);
      result = result * 37 + roles.hashCode();
      super.hashCode = result;
    }
    return result;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    if (id != null) builder.append(", id=").append(id);
    if (name != null) builder.append(", name=").append(name);
    if (!roles.isEmpty()) builder.append(", roles=").append(roles);
    return builder.replace(0, 2, "Proto3Sample{").append('}').toString();
  }

  public static final class Builder extends Message.Builder<Proto3Sample, Builder> {
    public Long id;

    public String name;

    public List<String> roles;

    public Builder() {
      roles = Internal.newMutableList();
    }

    public Builder id(Long id) {
      this.id = id;
      return this;
    }

    public Builder name(String name) {
      this.name = name;
      return this;
    }

    public Builder roles(List<String> roles) {
      Internal.checkElementsNotNull(roles);
      this.roles = roles;
      return this;
    }

    @Override
    public Proto3Sample build() {
      return new Proto3Sample(id, name, roles, super.buildUnknownFields());
    }
  }

  private static final class ProtoAdapter_Proto3Sample extends ProtoAdapter<Proto3Sample> {
    public ProtoAdapter_Proto3Sample() {
      super(FieldEncoding.LENGTH_DELIMITED, Proto3Sample.class);
    }

    @Override
    public int encodedSize(Proto3Sample value) {
      return ProtoAdapter.INT64.encodedSizeWithTag(1, value.id)
          + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name)
          + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles)
          + value.unknownFields().size();
    }

    @Override
    public void encode(ProtoWriter writer, Proto3Sample value) throws IOException {
      ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id);
      ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name);
      ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles);
      writer.writeBytes(value.unknownFields());
    }

    @Override
    public Proto3Sample decode(ProtoReader reader) throws IOException {
      Builder builder = new Builder();
      long token = reader.beginMessage();
      for (int tag; (tag = reader.nextTag()) != -1;) {
        switch (tag) {
          case 1: builder.id(ProtoAdapter.INT64.decode(reader)); break;
          case 2: builder.name(ProtoAdapter.STRING.decode(reader)); break;
          case 3: builder.roles.add(ProtoAdapter.STRING.decode(reader)); break;
          default: {
            reader.readUnknownField(tag);
          }
        }
      }
      builder.addUnknownFields(reader.endMessageAndGetUnknownFields(token));
      return builder.build();
    }

    @Override
    public Proto3Sample redact(Proto3Sample value) {
      Builder builder = value.newBuilder();
      builder.clearUnknownFields();
      return builder.build();
    }
  }
}
```

proto3では繰り返しのない全てのフィールドは`optional`扱いでnullが入る可能性がありますが、`--android-annotations`オプションをつけても残念ながら`@Nullable`アノテーションは追加されませんでした。現時点では明示的に`optional`指定された場合のみアノテーションがつくような挙動になっているようです。
やはり、proto3対応に関しては今後に期待ということになります。

### Kotlin

```kotlin:Proto3Sample.kt
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: proto3_sample.proto
package com.github.kazukinr.sample.proto

import android.os.Parcelable
import com.squareup.wire.AndroidMessage
import com.squareup.wire.FieldEncoding
import com.squareup.wire.ProtoAdapter
import com.squareup.wire.ProtoReader
import com.squareup.wire.ProtoWriter
import com.squareup.wire.WireField
import kotlin.Any
import kotlin.AssertionError
import kotlin.Boolean
import kotlin.Deprecated
import kotlin.DeprecationLevel
import kotlin.Int
import kotlin.Long
import kotlin.Nothing
import kotlin.String
import kotlin.collections.List
import kotlin.hashCode
import kotlin.jvm.JvmField
import okio.ByteString

class Proto3Sample(
  @field:WireField(
    tag = 1,
    adapter = "com.squareup.wire.ProtoAdapter#INT64"
  )
  val id: Long? = null,
  @field:WireField(
    tag = 2,
    adapter = "com.squareup.wire.ProtoAdapter#STRING"
  )
  val name: String? = null,
  @field:WireField(
    tag = 3,
    adapter = "com.squareup.wire.ProtoAdapter#STRING",
    label = WireField.Label.REPEATED
  )
  val roles: List<String> = emptyList(),
  unknownFields: ByteString = ByteString.EMPTY
) : AndroidMessage<Proto3Sample, Nothing>(ADAPTER, unknownFields) {
  @Deprecated(
    message = "Shouldn't be used in Kotlin",
    level = DeprecationLevel.HIDDEN
  )
  override fun newBuilder(): Nothing = throw AssertionError()

  override fun equals(other: Any?): Boolean {
    if (other === this) return true
    if (other !is Proto3Sample) return false
    return unknownFields == other.unknownFields
        && id == other.id
        && name == other.name
        && roles == other.roles
  }

  override fun hashCode(): Int {
    var result = super.hashCode
    if (result == 0) {
      result = unknownFields.hashCode()
      result = result * 37 + id.hashCode()
      result = result * 37 + name.hashCode()
      result = result * 37 + roles.hashCode()
      super.hashCode = result
    }
    return result
  }

  override fun toString(): String {
    val result = mutableListOf<String>()
    if (id != null) result += """id=$id"""
    if (name != null) result += """name=$name"""
    if (roles.isNotEmpty()) result += """roles=$roles"""
    return result.joinToString(prefix = "Proto3Sample{", separator = ", ", postfix = "}")
  }

  fun copy(
    id: Long? = this.id,
    name: String? = this.name,
    roles: List<String> = this.roles,
    unknownFields: ByteString = this.unknownFields
  ): Proto3Sample = Proto3Sample(id, name, roles, unknownFields)

  companion object {
    @JvmField
    val ADAPTER: ProtoAdapter<Proto3Sample> = object : ProtoAdapter<Proto3Sample>(
      FieldEncoding.LENGTH_DELIMITED, 
      Proto3Sample::class
    ) {
      override fun encodedSize(value: Proto3Sample): Int = 
        ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) +
        ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) +
        ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) +
        value.unknownFields.size

      override fun encode(writer: ProtoWriter, value: Proto3Sample) {
        ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id)
        ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name)
        ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles)
        writer.writeBytes(value.unknownFields)
      }

      override fun decode(reader: ProtoReader): Proto3Sample {
        var id: Long? = null
        var name: String? = null
        val roles = mutableListOf<String>()
        val unknownFields = reader.forEachTag { tag ->
          when (tag) {
            1 -> id = ProtoAdapter.INT64.decode(reader)
            2 -> name = ProtoAdapter.STRING.decode(reader)
            3 -> roles.add(ProtoAdapter.STRING.decode(reader))
            else -> reader.readUnknownField(tag)
          }
        }
        return Proto3Sample(
          id = id,
          name = name,
          roles = roles,
          unknownFields = unknownFields
        )
      }

      override fun redact(value: Proto3Sample): Proto3Sample = value.copy(
        unknownFields = ByteString.EMPTY
      )
    }

    @JvmField
    val CREATOR: Parcelable.Creator<Proto3Sample> = AndroidMessage.newCreator(ADAPTER)
  }
}
```

Kotlinコードとして出力した場合、きちんと`optional`として出力されました。null安全という観点だけで言えば、proto3を出力する場合はKotlinのほうが安全です（が、Kotlin生成自体がリリース直後のため、その他の安定性では劣ると思います）。

冒頭でも記述しましたが、Kotlin対応、proto3対応に関しては現在進行形でいろいろと対応が進んでいるようなので、引き続き変更を追っていこうと思います。