#はじめに
Kotlin Serialization ガイドの目次に基づいて「Kotlin Serialization guide」を訳しています。
なお、翻訳にはDeepLの力を99%借りています。
もし、一緒に「Kotlin Serialization ガイド」の翻訳をして下さる方がいらっしゃいましたら、コメント欄などから連絡をください。
第6章 代替フォーマットとカスタムフォーマット(実験)
これは Kotlin Serialization Guide の第6章です。JSONを超えて、代替フォーマットやカスタムフォーマットを網羅しています。安定している JSON とは異なり、これらは現在 Kotlin Serialization の実験的な機能です。
目次
CBOR(実験)
CBORはJSONの標準コンパクトバイナリエンコーディングの1つなので、JSONの機能のサブセットをサポートしており、一般的には使用中のJSONと非常によく似ていますが、バイナリデータを生成します。
CBOR のサポートは (実験的に) 別の
org.jetbrains.kotlinx:kotlinx-serialization-cbor:<version>
モジュールで利用可能です。
CborクラスにはCbor.encodeToByteArrayとCbor.decodeFromByteArray関数があります。
ここでは、JSONエンコーディングの基本的な例を見ながら、CBORを使ってエンコードしてみましょう。
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
val bytes = Cbor.encodeToByteArray(data)
println(bytes.toAsciiHexString())
val obj = Cbor.decodeFromByteArray<Project>(bytes)
println(obj)
}
完全なコードはこちらから取得できます。
非ASCIIデータを16進数で書き出し、フィルタリングされたASCII表現を出力しています。
{BF}dnameukotlinx.serializationhlanguagefKotlin{FF}
Project(name=kotlinx.serialization, language=Kotlin)
CBOR 16進数表記では、以下のように出力されます。
BF # map(*)
64 # text(4)
6E616D65 # "name"
75 # text(21)
6B6F746C696E782E73657269616C697A6174696F6E # "kotlinx.serialization"
68 # text(8)
6C616E6775616765 # "language"
66 # text(6)
4B6F746C696E # "Kotlin"
FF # primitive(*)
注意: フォーマットとしての CBOR は JSON とは異なり、非自明なキーを持つマップをサポートしています (JSON の回避策については 構造化されたマップキーの使用許可 のセクションを参照してください)。
未知のキーを無視
CBORフォーマットは、デバイスのAPI進化の一環として新しいプロパティが追加される可能性があるIoTデバイスとの通信によく使用される。既定では、デシリアライズ中に未知のキーが検出されるとエラーが発生します。この動作は、ignoreUnknownKeysプロパティで設定できます。
val format = Cbor { ignoreUnknownKeys = true }
@Serializable
data class Project(val name: String)
fun main() {
val data = format.decodeFromHexString<Project>(
"bf646e616d65756b6f746c696e782e73657269616c697a6174696f6e686c616e6775616765664b6f746c696eff"
)
println(data)
}
完全なコードはこちらから取得できます。
Project
が language
プロパティを欠いているにもかかわらず、オブジェクトをデコードします。
Project(name=kotlinx.serialization)
CBOR 16進数表記では、入力は以下のようになります。
BF # map(*)
64 # text(4)
6E616D65 # "name"
75 # text(21)
6B6F746C696E782E73657269616C697A6174696F6E # "kotlinx.serialization"
68 # text(8)
6C616E6775616765 # "language"
66 # text(6)
4B6F746C696E # "Kotlin"
FF # primitive(*)
バイト配列とCBORデータ型
RFC 7049 Major Typesのセクションでは、CBORは以下のデータタイプをサポートしています。
- メジャー型 0:符号なし整数
- メジャータイプ1:負の整数
- 主要なタイプ 2: バイト文字列**。
- メジャータイプ3:テキスト文字列
- 主な型4:データ項目の配列**。
- メジャータイプ5:データ項目のペアのマップ
- メジャータイプ6:他のメジャータイプの任意の意味タグ付け
- メジャー型7:浮動小数点数と内容のない単純なデータ型、および「ブレーク」の停止コード
デフォルトでは、Kotlin の ByteArray
インスタンスは メジャータイプ 4 としてエンコードされます。メジャータイプ2**が必要な場合は、@ByteString
アノテーションを使用することができます。
@Serializable
data class Data(
@ByteString
val type2: ByteArray, // CBOR Major type 2
val type4: ByteArray // CBOR Major type 4
)
fun main() {
val data = Data(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8))
val bytes = Cbor.encodeToByteArray(data)
println(bytes.toAsciiHexString())
val obj = Cbor.decodeFromByteArray<Data>(bytes)
println(obj)
}
完全なコードはこちらから取得できます。
このように、データに先行するCBORバイトは、エンコーディングの種類によって異なります。
{BF}etype2D{01}{02}{03}{04}etype4{9F}{05}{06}{07}{08}{FF}{FF}
Data(type2=[1, 2, 3, 4], type4=[5, 6, 7, 8])
CBOR 16進数表記では、以下のように出力されます。
BF # map(*)
65 # text(5)
7479706532 # "type2"
44 # bytes(4)
01020304 # "\x01\x02\x03\x04"
65 # text(5)
7479706534 # "type4"
9F # array(*)
05 # unsigned(5)
06 # unsigned(6)
07 # unsigned(7)
08 # unsigned(8)
FF # primitive(*)
FF # primitive(*)
プロトバッファ (実験)
Protocol Buffers は言語ニュートラルなバイナリフォーマットで、通常はプロトコルスキーマを定義する別の".proto "ファイルに依存します。名前の代わりに整数をフィールドに割り当てるため、CBORよりもコンパクトです。
プロトコルバッファのサポートは、(実験的に)別の
org.jetbrains.kotlinx:kotlinx-serialization-protobuf:<version>
モジュールで利用可能です。
Kotlin Serialization は proto2 セマンティクスを使用しており、すべてのフィールドは明示的に必須または任意である。基本的な例として、ProtoBuf.encodeToByteArrayとProtoBuf.decodeFromByteArray関数を持つProtoBufクラスを使用するように変更します。
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
val bytes = ProtoBuf.encodeToByteArray(data)
println(bytes.toAsciiHexString())
val obj = ProtoBuf.decodeFromByteArray<Project>(bytes)
println(obj)
}
完全なコードはこちらから取得できます。
{0A}{15}kotlinx.serialization{12}{06}Kotlin
Project(name=kotlinx.serialization, language=Kotlin)
ProtoBuf 16進表記では、以下のように出力されます。
Field #1: 0A String Length = 21, Hex = 15, UTF8 = "kotlinx.serialization"
Field #2: 12 String Length = 6, Hex = 06, UTF8 = "Kotlin"
フィールド番号
デフォルトでは、Kotlin Serialization ProtoBuf実装のフィールド番号は自動的に割り当てられますが、これでは時間の経過とともに進化する安定したデータスキーマを定義する機能がありません。これは通常、別の ".proto" ファイルを書くことで実現されます。しかし、Kotlin Serializationでは、別のスキーマファイルを書くことなく、代わりに ProtoNumber アノテーションを使ってこの機能を得ることができます。
@Serializable
data class Project(
@ProtoNumber(1)
val name: String,
@ProtoNumber(3)
val language: String
)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
val bytes = ProtoBuf.encodeToByteArray(data)
println(bytes.toAsciiHexString())
val obj = ProtoBuf.decodeFromByteArray<Project>(bytes)
println(obj)
}
完全なコードはこちらから取得できます。
出力を見ると、最初のプロパティ name
の番号は変更されていませんが (デフォルトでは 1 から番号が付けられているため)、language
プロパティの番号は変更されています。
{0A}{15}kotlinx.serialization{1A}{06}Kotlin
Project(name=kotlinx.serialization, language=Kotlin)
ProtoBuf 16進表記では、以下のように出力されます。
Field #1: 0A String Length = 21, Hex = 15, UTF8 = "kotlinx.serialization" (total 21 chars)
Field #3: 1A String Length = 6, Hex = 06, UTF8 = "Kotlin"
整数型
プロトコルバッファは、異なる整数範囲に最適化された様々な整数エンコーディングをサポートしています。これらは、ProtoTypeアノテーションとProtoIntegerType列挙を使用して指定されます。次の例では、サポートされている3つのオプションをすべて示しています。
@Serializable
class Data(
@ProtoType(ProtoIntegerType.DEFAULT)
val a: Int,
@ProtoType(ProtoIntegerType.SIGNED)
val b: Int,
@ProtoType(ProtoIntegerType.FIXED)
val c: Int
)
fun main() {
val data = Data(1, -2, 3)
println(ProtoBuf.encodeToByteArray(data).toAsciiHexString())
}
完全なコードはこちらから取得できます。
-
defaultは、小さな非負数用に最適化された varint エンコーディング (
intXX
) です。1の値は、1バイトの
01` でエンコードされる。 -
signedは符号付きZigZagエンコーディング(
sintXX
)であり、小さな符号付き整数用に最適化されています。2
の値は 1 バイトの03
にエンコードされる。 -
固定エンコーディング(
fixedXX
)は、常に固定のバイト数を使用します。
3
の値は03 00 00 00
の4バイトでエンコードされる。
uintXX
および
sfixedXX` プロトコルバッファ型はサポートされていません。
{08}{01}{10}{03}{1D}{03}{00}{00}{00}
ProtoBuf 16進表記では、以下のように出力されます。
Field #1: 08 Varint Value = 1, Hex = 01
Field #2: 10 Varint Value = 3, Hex = 03
Field #3: 1D Fixed32 Value = 3, Hex = 03-00-00-00
繰り返しのフィールドとしてリスト
Kotlinのリストやその他のコレクションは、繰り返しフィールドとして表現されます。プロトコルバッファでは、リストが空の場合、対応する番号の要素がストリーム内に存在しません。Kotlin Serializationでは、コレクションやマップ型のプロパティに emptyList()
のデフォルト値を明示的に指定する必要があります。そうでなければ、空のリストをデシリアライズすることができません。これは、プロトコルバッファでは欠落したフィールドと区別がつきません。
@Serializable
data class Data(
val a: List<Int> = emptyList(),
val b: List<Int> = emptyList()
)
fun main() {
val data = Data(listOf(1, 2, 3), listOf())
val bytes = ProtoBuf.encodeToByteArray(data)
println(bytes.toAsciiHexString())
println(ProtoBuf.decodeFromByteArray<Data>(bytes))
}
完全なコードはこちらから取得できます。
{08}{01}{08}{02}{08}{03}
Data(a=[1, 2, 3], b=[])
パックされた繰り返しフィールドはサポートされていません。
ProtoBuf診断モードでは、以下のような出力になります。
Field #1: 08 Varint Value = 1, Hex = 01
Field #1: 08 Varint Value = 2, Hex = 02
Field #1: 08 Varint Value = 3, Hex = 03
プロパティ(実験)
Kotlin Serializationは、[Properties] kotlinx.serialization.properties.Properties形式の実装を介して、String
キーを持つフラットマップにクラスをシリアライズすることができます。
プロパティのサポートは、(実験的に)別の
org.jetbrains.kotlinx:kotlinx-serialization-properties:<version>
モジュールで利用可能です。
@Serializable
class Project(val name: String, val owner: User)
@Serializable
class User(val name: String)
fun main() {
val data = Project("kotlinx.serialization", User("kotlin"))
val map = Properties.encodeToMap(data)
map.forEach { (k, v) -> println("$k = $v") }
}
完全なコードはこちらから取得できます。
結果として得られるマップは、入れ子になったオブジェクトのキーを表すドット区切りのキーを持っています。
name = kotlinx.serialization
owner.name = kotlin
カスタムフォーマット (実験)
Kotlin Serialization用のカスタムフォーマットは、Serializersの章で使用したEncoderとDecoderインターフェースの実装を提供しなければなりません。
これらはかなり大きなインターフェースです。便利なように、AbstractEncoderとAbstractDecoderのスケルトン実装が提供されています。AbstractEncoderでは、ほとんどの encodeXxx
メソッドはデフォルトで [encodeValue(value: Any)
] AbstractEncoder.encodeValue — にデリゲートする実装を持っており、基本的な作業形式を取得するために実装しなければならない唯一のメソッドです。
基本的なエンコーダ
まずは、ソースコードに書かれている順にデータをプリミティブな構成オブジェクトの単一のリストにエンコードするという、些細なフォーマットの実装から始めてみましょう。まず、AbstractEncoderのencodeValue
をオーバーライドして、簡単なEncoderを実装します。
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()
override val serializersModule: SerializersModule = EmptySerializersModule
override fun encodeValue(value: Any) {
list.add(value)
}
}
ここで、オブジェクトをエンコードしてリストを返すエンコーダを作成する便利なトップレベル関数を書きます。
fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any> {
val encoder = ListEncoder()
encoder.encodeSerializableValue(serializer, value)
return encoder.list
}
シリアライザを明示的に渡さなくてもよいように、encodeToList
関数の inline
オーバーロードに reified
型パラメータを指定して serializer 関数を用いて、実際の型に適した KSerializer インスタンスを取得するようにしています。
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)
これでテストできるようになりました。
@Serializable
data class Project(val name: String, val owner: User, val votes: Int)
@Serializable
data class User(val name: String)
fun main() {
val data = Project("kotlinx.serialization", User("kotlin"), 9000)
println(encodeToList(data))
}
完全なコードはこちらから取得できます。
その結果、オブジェクトグラフ内のすべてのプリミティブ値を取得し、_serial_順にリストに入れました。
[kotlinx.serialization, kotlin, 9000]
それ自体は、シリアライズ可能なオブジェクトツリーに含まれるすべてのデータに対して、ある種のハッシュコードやダイジェストを計算する必要がある場合に便利な機能です。
基本的なデコーダ
デコーダはもっと実体のあるものを実装する必要があります。
- decodeValue — リストの中から次の値を返します。
-
decodeElementIndex — デシリアライズされた値の次のインデックスを返します。
このプリミティブ形式では、デシリアライズは常に順番に行われるので、変数elementIndex
にインデックスを記録しておきます。これがどのように使われるかについては、手書き複合シリアライザのセクションを参照してください。 -
beginStructure — 再帰的にデコードされる各構造体がそれ自身の
elementIndex
状態を別々に追跡するように、ListDecoder
の新しいインスタンスを返します。
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
private var elementIndex = 0
override val serializersModule: SerializersModule = EmptySerializersModule
override fun decodeValue(): Any = list.removeFirst()
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementIndex == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
return elementIndex++
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
ListDecoder(list)
}
デコードに便利な機能がいくつかあります。
fun <T> decodeFromList(list: List<Any>, deserializer: DeserializationStrategy<T>): T {
val decoder = ListDecoder(ArrayDeque(list))
return decoder.decodeSerializableValue(deserializer)
}
inline fun <reified T> decodeFromList(list: List<Any>): T = decodeFromList(list, serializer())
これだけで、基本的なシリアライズ可能なクラスのエンコードとデコードを開始することができます。
fun main() {
val data = Project("kotlinx.serialization", User("kotlin"), 9000)
val list = encodeToList(data)
println(list)
val obj = decodeFromList<Project>(list)
println(obj)
}
フルコードはこちら取得することができます。
これで、プリミティブのリストをオブジェクトツリーに戻すことができます。
[kotlinx.serialization, kotlin, 9000]
Project(name=kotlinx.serialization, owner=User(name=kotlin), votes=9000)
逐次復号化
我々が実装したデコーダは elementIndex
の状態を追跡して decodeElementIndex
を実装している。つまり、手書き複合シリアライザの項で書いた単純なシリアライザでも、任意のシリアライザで動作するようになるということです。しかし、このフォーマットは常に要素を順番に格納するので、このブックキーピングは必要なく、デコード性能を損なう。JVM上のすべての自動生成シリアライザは逐次デコーディングプロトコル(エクスペリメンタル)をサポートしており、デコーダはCompositeDecoder.decodeSequentially関数からtrue
を返すことでサポートしていることを示すことができます。
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
private var elementIndex = 0
override val serializersModule: SerializersModule = EmptySerializersModule
override fun decodeValue(): Any = list.removeFirst()
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementIndex == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
return elementIndex++
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
ListDecoder(list)
override fun decodeSequentially(): Boolean = true
}
完全なコードはこちらから取得できます。
コレクションサポートの追加
この基本的なフォーマットでは、今のところコレクションを適切に表現することはできません。In はエンコードしますが、コレクションの中に何個の要素があるのか、どこで終わるのかを記録していないので、正しくデコードすることができません。まず、Encoder.beginCollection 関数を実装して、エンコーダにコレクションの適切なサポートを追加してみましょう。関数 beginCollection
はコレクションのサイズをパラメータとして受け取るので、それをエンコードして結果に追加します。私たちのエンコーダの実装は状態を保持しないので、beginCollection
関数から this
を返すだけです。
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()
override val serializersModule: SerializersModule = EmptySerializersModule
override fun encodeValue(value: Any) {
list.add(value)
}
override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
encodeInt(collectionSize)
return this
}
}
私たちの場合、デコーダは、以前のコードに加えて CompositeDecoder.decodeCollectionSize 関数を実装する必要があります。
コレクションサイズをあらかじめ格納するフォーマットは、
decodeSequentially
からtrue
を返さなければならない。
class ListDecoder(val list: ArrayDeque<Any>, var elementsCount: Int = 0) : AbstractDecoder() {
private var elementIndex = 0
override val serializersModule: SerializersModule = EmptySerializersModule
override fun decodeValue(): Any = list.removeFirst()
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
return elementIndex++
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
ListDecoder(list, descriptor.elementsCount)
override fun decodeSequentially(): Boolean = true
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
decodeInt().also { elementsCount = it }
}
コレクションやMapをサポートするために必要なのはそれだけです。
@Serializable
data class Project(val name: String, val owners: List<User>, val votes: Int)
@Serializable
data class User(val name: String)
fun main() {
val data = Project("kotlinx.serialization", listOf(User("kotlin"), User("jetbrains")), 9000)
val list = encodeToList(data)
println(list)
val obj = decodeFromList<Project>(list)
println(obj)
}
フルコードはこちらから取得することができます。
デコーダに停止場所を知らせるために、結果に追加されたリストのサイズを見ることができます。
[kotlinx.serialization, 2, kotlin, jetbrains, 9000]
Project(name=kotlinx.serialization, owners=[User(name=kotlin), User(name=jetbrains)], votes=9000)
nullサポートの追加
私たちのトリビアルフォーマットは今のところ null
値をサポートしていません。null可能な型には、何らかの "nullインジケータ "を追加する必要があります。
エンコーダの実装では、Encoder.encodeNullとEncoder.encodeNotNullMarkをオーバーライドしています。
override fun encodeNull() = encodeValue("NULL")
override fun encodeNotNullMark() = encodeValue("!!")
デコーダの実装では、Decoder.decodeNotNullMarkをオーバーライドしています。
override fun decodeNotNullMark(): Boolean = decodeString() != "NULL"
NULLではない値とNULL値の両方でNULL可能なプロパティをテストしてみましょう。
@Serializable
data class Project(val name: String, val owner: User?, val votes: Int?)
@Serializable
data class User(val name: String)
fun main() {
val data = Project("kotlinx.serialization", User("kotlin") , null)
val list = encodeToList(data)
println(list)
val obj = decodeFromList<Project>(list)
println(obj)
}
完全なコードはこちらから取得できます。
出力では、NOT-NULLマークとNULLマークがどのように使われているかがわかります。
[kotlinx.serialization, !!, kotlin, NULL]
Project(name=kotlinx.serialization, owner=User(name=kotlin), votes=null)
効率的なバイナリ形式
これで、効率的なバイナリ形式の例の準備ができました。java.io.DataOutputの実装にデータを書き込もうとしています。エンコーダ内の10個のプリミティブのそれぞれに対して、encodeValue
の代わりに個々のencodeXxx
関数をオーバーライドしなければなりません。
class DataOutputEncoder(val output: DataOutput) : AbstractEncoder() {
override val serializersModule: SerializersModule = EmptySerializersModule
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
override fun encodeShort(value: Short) = output.writeShort(value.toInt())
override fun encodeInt(value: Int) = output.writeInt(value)
override fun encodeLong(value: Long) = output.writeLong(value)
override fun encodeFloat(value: Float) = output.writeFloat(value)
override fun encodeDouble(value: Double) = output.writeDouble(value)
override fun encodeChar(value: Char) = output.writeChar(value.toInt())
override fun encodeString(value: String) = output.writeUTF(value)
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = output.writeInt(index)
override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
encodeInt(collectionSize)
return this
}
override fun encodeNull() = encodeBoolean(false)
override fun encodeNotNullMark() = encodeBoolean(true)
}
デコーダの実装はエンコーダの実装をミラーしており、すべてのプリミティブな decodeXxx
関数をオーバーライドします。
class DataInputDecoder(val input: DataInput, var elementsCount: Int = 0) : AbstractDecoder() {
private var elementIndex = 0
override val serializersModule: SerializersModule = EmptySerializersModule
override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0
override fun decodeByte(): Byte = input.readByte()
override fun decodeShort(): Short = input.readShort()
override fun decodeInt(): Int = input.readInt()
override fun decodeLong(): Long = input.readLong()
override fun decodeFloat(): Float = input.readFloat()
override fun decodeDouble(): Double = input.readDouble()
override fun decodeChar(): Char = input.readChar()
override fun decodeString(): String = input.readUTF()
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.readInt()
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
return elementIndex++
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
DataInputDecoder(input, descriptor.elementsCount)
override fun decodeSequentially(): Boolean = true
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
decodeInt().also { elementsCount = it }
override fun decodeNotNullMark(): Boolean = decodeBoolean()
}
これで任意のデータをシリアライズしたり デシリアライズしたりできるようになりました。例えば、CBOR (experimental)やProtoBuf (experimental)で使われていたクラスと同じです。
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
val output = ByteArrayOutputStream()
encodeTo(DataOutputStream(output), data)
val bytes = output.toByteArray()
println(bytes.toAsciiHexString())
val input = ByteArrayInputStream(bytes)
val obj = decodeFrom<Project>(DataInputStream(input))
println(obj)
}
完全なコードはこちらから取得できます。
ご覧のように、結果は、シリアル化されているデータのみを含む密なバイナリ形式になります。任意のドメイン固有のコンパクトエンコーディングのために簡単に微調整することができます。
{00}{15}kotlinx.serialization{00}{06}Kotlin
Project(name=kotlinx.serialization, language=Kotlin)
フォーマット固有の型
フォーマットの実装では、Kotlin Serialization のプリミティブ型のリストにないデータ型や、対応する encodeXxx
/decodeXxx
関数を持たないデータ型に対して特別なサポートを提供することがあります。"エンコーダでは、encodeSerializableValue(serializer, value)
関数をオーバーライドすることで実現します。
DataOutput`フォーマットの例では、DataOutputにはこの目的のための特別なメソッドがあるので、バイトの配列をシリアライズするための特別な効率的なデータパスを提供したいと思うかもしれません。
型の検出はシリアライズされる value
の型を確認するのではなく、serializer
を見ることで行われるので、ビルドインの KSerializer インスタンスから ByteArray
型を取得する。
これは重要な違いです。このようにして、私たちのフォーマットの実装は カスタムシリアライザ を適切にサポートしています。
private val byteArraySerializer = serializer<ByteArray>()
特にバイト配列については、組み込みの ByteArraySerializer 関数を使用することもできました。
効率的なバイナリフォーマットの実装であるEncoderに対応するコードを追加する。エンコーディングをより効率的にするために、ByteArray
の実装に encodeCompactSize
関数を追加する。
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
if (serializer === byteArraySerializer)
encodeByteArray(value as ByteArray)
else
super.encodeSerializableValue(serializer, value)
}
private fun encodeByteArray(bytes: ByteArray) {
encodeCompactSize(bytes.size)
output.write(bytes)
}
private fun encodeCompactSize(value: Int) {
if (value < 0xff) {
output.writeByte(value)
} else {
output.writeByte(0xff)
output.writeInt(value)
}
}
Decoderの実装にも同様のコードが追加されています。ここでは、decodeSerializableValue関数をオーバーライドします。
@Suppress("UNCHECKED_CAST")
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>, previousValue: T?): T =
if (deserializer === byteArraySerializer)
decodeByteArray() as T
else
super.decodeSerializableValue(deserializer, previousValue)
private fun decodeByteArray(): ByteArray {
val bytes = ByteArray(decodeCompactSize())
input.readFully(bytes)
return bytes
}
private fun decodeCompactSize(): Int {
val byte = input.readByte().toInt() and 0xff
if (byte < 0xff) return byte
return input.readInt()
}
これで、いくつかのバイト配列のシリアライズを実行する準備が整いました。
@Serializable
data class Project(val name: String, val attachment: ByteArray)
fun main() {
val data = Project("kotlinx.serialization", byteArrayOf(0x0A, 0x0B, 0x0C, 0x0D))
val output = ByteArrayOutputStream()
encodeTo(DataOutputStream(output), data)
val bytes = output.toByteArray()
println(bytes.toAsciiHexString())
val input = ByteArrayInputStream(bytes)
val obj = decodeFrom<Project>(DataInputStream(input))
println(obj)
}
フルコードはこちらから取得することができます。
ご覧のように、カスタムのバイト配列フォーマットが使用されており、そのサイズを1バイトでコンパクトにエンコードしています。
{00}{15}kotlinx.serialization{04}{0A}{0B}{0C}{0D}
Project(name=kotlinx.serialization, attachment=[10, 11, 12, 13])
本章では、Kotlin Serialization ガイドを締めくくります。