Kotlinで利用可能なJSONライブラリーには「GSON, Jackson, Moshi, Kotlin serializationなど」があります。
Kotlin Serializationは、Kotlinがベース(前の3つはJavaがベース)です。
2020.10にVer 1.0.0がリリースされており、比較的新しいライブラリです。
今回は、Kotlin serializationについて、まとめます。
※環境:Android Studio Ladybug Feature Drop | 2024.2.2 Patch 1
Kotlin 2.0.0
Kotlin serialization json 1.7.1
目次
Kotlin serializationとは
Kotlin serializationは、Kotlin上で動作するJSONライブラリーです。
Kotlin serializationを使うと、「JSONの記述⇔クラスのオブジェクト」といった相互変換(パース:解析して変換)が簡単に出来ます。
プログラム間(コンピュータ間や異なるプログラミング言語間など)でデータを受け渡す際に、データのフォーマットとしてJSONが広く使われています。
その一例は、Retrofit(REST準拠のAPI)です。
RetrofitでKotlin Serializationを利用すれば、HTTPプロトコルでサーバーにデータを要求し、返答をJSONフォーマットで受け取り、クラスのオブジェクトでプログラムへ取り込むことが可能です。
さらに、Kotlin serializationはJSON以外のフォーマットも扱えます。
※フォーマットについては「Serialization formats」を参照
※HOCON(Human-Optimized Config Object Notation)
※ProtoBuf(Protocol Buffers )
※CBOR(Concise Binary Object Representation)
※Properties(Java Properties)
Kotlin serializationはJSONに特化していないことから、フォーマットを扱うというよりも、シリアル化の機能を重視したライブラリーのようです。
※シリアル化については「シリアル化(serialize)の意味と役割」を参照
環境設定
「app/build.gradle」へ次の設定を行います。
plugins { ... id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" // Ktolinのバージョンに合わせる // id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" // id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" } android { ... } dependencies { ... implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") // implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") // implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") }
Serializationプラグイン
Kotlinのリリースに合わせて、Serializationプラグインもリリースされます。
両者のバージョンは合わせるようにします。
プラグインのバージョンについてはリリースノートを参照して下さい。
Serializationライブラリ
Kotlinのバージョンに対応するSerializationライブラリーがあります。
対応するライブラリーを使った方が良さそうです。※ドキュメントに指示は無かったと思いますが…
ライブラリーのバージョンについてはリリースノートを参照して下さい。
基本的なSerializationの動作(例:プリミティブ型)
Android Studioはビルドをする際に、@Serializable付きのデータクラスを認識して、データクラスのJavaバイナリへ、実装済みSerializerクラスを埋め込みます。
@Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String // JSONのキーに別名を定義 )
Serializerクラス(KSerializerインターフェース)は、serialize( )とdeserialize( )関数を持ちます。両者は相互変化で呼び出される関数です。変換の方法が定義されています。
object XXXSerializer : KSerializer<XXX> { override val descriptor: SerialDescriptor get() = TODO("識別子を実装") override fun serialize(encoder: Encoder, value: XXX) { // Json.encodeToString( )で呼び出される // Obj->JSONの変換を実装 } override fun deserialize(decoder: Decoder): XXX { // Json.decodeFromString( )で呼び出される // JSON->Objの変換を実装 return XXX() } }
JSON⇒Obj変換
Json#decodeFromStringを呼び出します。
内部で対象クラスのdeserialize( )が呼び出されます。
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager" } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager)
Obj⇒JSON変換
Json#encodeToStringを呼び出します。
内部で対象クラスのserialize( )が呼び出されます。
val _oPerson = Person(id = 2, name = "Droid", post = "Staff") val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff"}
Jsonはユーティリティークラス
Jsonクラスは自身を継承したコンパニオンオブジェクト(クラス内オブジェクト)を持ちます。
ですので、Json.encodeToString( )という形式で実行すると、Jsonオブジェクトはシングルトンになり、Jsonクラスの関数はコンパニオンオブジェクトの関数になります。
そして、Jsonクラスの関数はユーティリティークラスの関数と同じ扱いになります。
public sealed class Json( public val configuration: JsonConfiguration, override val serializersModule: SerializersModule ) : StringFormat { ... @ThreadLocal // to support caching on K/N @OptIn(ExperimentalSerializationApi::class) public companion object Default : Json(JsonConfiguration(), EmptySerializersModule()) ... public final override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String { val result = JsonToStringWriter() try { encodeByWriter(this@Json, result, serializer, value) return result.toString() } finally { result.release() } } ... public inline fun <reified T> decodeFromString(@FormatLanguage("json", "", "") string: String): T = decodeFromString(serializersModule.serializer(), string) ... public final override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, @FormatLanguage("json", "", "") string: String): T { val lexer = StringJsonLexer(this, string) val input = StreamingJsonDecoder(this, WriteMode.OBJ, lexer, deserializer.descriptor, null) val result = input.decodeSerializableValue(deserializer) lexer.expectEof() return result } ... }
例:Array
Array型のプロパティを持つ場合の相互変換です。
@Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String, // JSONのキーに別名を定義 val times: Array<Int> )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":[32122, 43290, 46662, 68756] } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager, times=[32122, 43290, 46662, 68756])Obj⇒JSON変換
val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = arrayOf(28207, 43320, 46750, 62159) ) val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff","times":[28207,43320,46750,62159]}
例:List
List型のプロパティを持つ場合の相互変換です。
@Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String, // JSONのキーに別名を定義 val times: List<Int> )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":[32122, 43290, 46662, 68756] } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager, times=[32122, 43290, 46662, 68756])Obj⇒JSON変換
val _oPerson = Person3( id = 2, name = "Droid", post = "Staff", times = listOf(28207, 43320, 46750, 62159) ) val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff","times":[28207,43320,46750,62159]}
例:Map
Map型のプロパティを持つ場合の相互変換です。
@Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String, // JSONのキーに別名を定義 val times: Map<Int, String> )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":{"32122":"login","43290":"pause", "46662":"resume", "68756":"logout"} } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager, times={32122=login, 43290=pause, 46662=resume, 68756=logout})Obj⇒JSON変換
val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = mapOf(28207 to "login", 43320 to "pause", 46750 to "resume", 62159 to "login") ) val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff","times":{"28207":"login","43320":"pause","46750":"resume","62159":"login"}}
例:Set
Set型のプロパティを持つ場合の相互変換です。
@Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String, // JSONのキーに別名を定義 val times: Set<Int> )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":[32122, 43290, 46662, 68756] } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager, times=[32122, 43290, 46662, 68756])Objス⇒JSON変換
val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = setOf(28207, 43320, 46750, 62159) ) val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff","times":[28207,43320,46750,62159]}
例:Enum
Enum型のプロパティを持つ場合の相互変換です。
enum class TimeSignal { LOGIN, LOGOUT, PAUSE, RESUME } @Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String, // JSONのキーに別名を定義 val times: TimeSignal )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":"LOGIN" } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager, times=LOGIN)Obj⇒JSON変換
val _oPerson = Person(id = 2, name = "Droid", post = "Staff", times = TimeSignal.LOGOUT) val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff","times":"LOGOUT"}
例:データクラス
データクラス型のプロパティを持つ場合の相互変換です。
@Serializable data class TimeStamp(val hour: Int, val min: Int, val sec: Int) @Serializable data class Person( val id: Int, val name: String, @SerialName("position") val post: String, // JSONのキーに別名を定義 val times: TimeStamp )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":{"hour":8,"min":55,"sec":22} } """.replace("\\s+".toRegex(), "") // 空白の削除 val _oPerson = Json.decodeFromString<Person>(_jPerson) Log.i(TAG, "(Serialization) Person Obj = ${_oPerson}")
(Serialization) Person Obj = Person(id=1, name=Android, post=Manager, times=TimeStamp(hour=8, min=55, sec=22))Obj⇒JSON変換
val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = TimeStamp(7, 50, 7) ) val _jPerson = Json.encodeToString(_oPerson) Log.i(TAG, "(Serialization) Person Json = ${_jPerson}")
(Serialization) Person Json = {"id":2,"name":"Droid","position":"Staff","times":{"hour":7,"min":50,"sec":7}}
nullable/non-nullとデフォルト値の対応
Kotolin serializationにおいて、nullable/non-nullとデフォルト値の対応は表のようになります。
JSONでkey:valueが無い(JSON⇒Obj変換)
JSONの記述でKey : Valueが欠けることは許されません。欠けているとExceptionになります。
ただし、デフォルト値が設定されていれば、デフォルト値で補われます。
JSONの記述 | ⇒ | データクラスの定義 | オブジェクト |
---|---|---|---|
{ "id":1, "name":"Android" } ※key : valueが無い | (変換) | data class Person( val id: Int, val name: String, val post: String ) | MissingFieldException |
data class Person( val id: Int, val name: String, val post: String? ) |
|||
data class Person( val id: Int, val name: String, val post: String = ”Staff” ) | id = 1 name = Android post = Staff |
||
data class Person( val id: Int, val name: String, val post: String? = null ) | id = 1 name = Android post = null |
kotlinx.serialization.MissingFieldException: Field 'post' is required for type with serial name 'Person', but it was missing at path
JSONでvalueがnull(JSON⇒Obj変換)
JSONの記述でvalueがnullの場合は、プロパティはnullable型が必須です。non-null型になっているとExceptionになります。
JSONの記述 | ⇒ | データクラスの定義 | オブジェクト |
---|---|---|---|
{ "id":1, "name":"Android", "post":null } ※valueがnull | (変換) | data class Person( val id: Int, val name: String, val post: String ) | JsonDecodingException |
data class Person( val id: Int, val name: String, val post: String? ) | id = 1 name = Android post = null |
||
data class Person( val id: Int, val name: String, val post: String = ”Staff” ) | JsonDecodingException ※1 |
||
data class Person( val id: Int, val name: String, val post: String? = null ) | id = 1 name = Android post = null |
||
※1:「coerceInputValues = true」で回避可能 |
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 47: Expected string literal but 'null' literal was found at path. Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value. JSON input: { "id":1, "name":"Android", "post":null }
プロパティがnull(Obj⇒JSON変換)
プロパティがnullの場合は、JSONの記述にそのままnullが出力されます。
データクラスの定義 | オブジェクト | ⇒ | JSONの記述 |
---|---|---|---|
data class Person( val id: Int, val name: String, val post: String? ) | id = 2 name = Droid post = null | (変換) | { "id":2, "name":"Droid" "post":null } |
関連記事: