この記事の内容
Square社のProtocol Buffers実装であるWireが、3.0.0でKotlinに対応しました。
これまでKotlinメインのプロジェクトでもproto生成ファイルだけはJavaコードを使用することが多かったと思いますが(実際、Kotlin protobuf
等で検索しても、出てくる記事の多くはJavaを使っていました)、Kotlin化できるかもー! ということで使ってみました。
この記事では、執筆時点での最新版 Wire 3.0.1 を使用しています。現在も絶賛開発中なので、導入される場合は本家サイトの状況をご確認ください。
Kotlinファイルの生成
公式では wire-gradle-plugin での導入が紹介されていますが、執筆時点では plugins { id 'com.squareup.wire' }
では参照できず classpath
+ apply plugin
形式で記述することでビルドできました(gradle plugin検索でもヒットしなかったので、まだ登録されていないようです)。
また、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
を同時に指定することはできません。
$ 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ファイルからそれぞれのファイルを生成してみます。
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
オプションをつけて生成した結果を記載します。
$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \
--proto_path=src/main/proto \
--java_out=src/main/java \
--android \
--android-annotations
出力結果
// 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
オプションの有無による生成ファイルの違いはありませんでした。
$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \
--proto_path=src/main/proto \
--java_out=src/main/java \
--android
出力結果
// 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にはデフォルト値が設定されているので、必要な項目だけ指定すれば大丈夫です。
// 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の更新も書き方が変わります。
// 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ファイルです。
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
// 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
// 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対応に関しては現在進行形でいろいろと対応が進んでいるようなので、引き続き変更を追っていこうと思います。