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 } |
関連記事:
