Kotlinで利用可能なJSONライブラリーには「GSON, Jackson, Moshi, Kotlin Serializationなど」があります。
始めの3つはJavaがベースです。その中のMoshiは、拡張機能によりKotlinとの親和性が高められています。
Kotlin SerializationはJSON以外(Protobuf, CBOR, Hocon, Properties )のフォーマットも扱えます。フォーマットを扱うというよりも、シリアル化の機能を重視したライブラリーのようです。
プログラム間でデータを受け渡す際に用いるのであれば、Kotlin Serializationが最も適しているかも知れません。
Kotlin Serializationに興味を引かれますが、後の機会に置いといて…
今回は、Moshiについて、まとめます。
※環境:Android Studio Ladybug Feature Drop | 2024.2.2
Kotlin 2.0.0
Moshi 1.15.2
目次
Moshiとは
MoshiはKotlin(もしくはJava)上で動作するJSONライブラリです。
Moshiを使うと、「JSONの記述⇔データクラスのオブジェクト」といった相互変換(パース:解析して変換)が簡単に出来ます。
プログラム間(コンピュータ間や異なるプログラミング言語間など)でデータを受け渡す際に、データのフォーマットとしてJSONが広く使われています。
その一例は、Retrofit(REST準拠のAPI)です。
RetrofitでMoshiを利用すれば、HTTPプロトコルでサーバーにデータを要求し、返答をJSONフォーマットで受け取り、データクラスのオブジェクトでプログラムへ取り込むことが可能です。
とても、便利です。
なお、MoshiのベースはJavaであり、KotlinのサポートはMoshi-Kotlin拡張機能によるものです。
環境設定
「app/build.gradle」へ次の設定を行います。
plugins { ... id("com.google.devtools.ksp") version "2.0.0-1.0.23" ... } android { ... } dependencies { ... val moshi_version = "1.15.2" implementation ("com.squareup.moshi:moshi:$moshi_version") implementation ("com.squareup.moshi:moshi-kotlin:$moshi_version") ksp ("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version") ... }
Kotlin上でMoshiを使うのであれば、Moshi-Kotlin拡張機能が必要です。
また、Moshiのアノテーションを処理するために、KSP(Kotlin Symbol Processor)を使います。
基本的なMoshiの動作(例:プリミティブ型)
KotlinはJavaと比較して、次のような違いがあります。
- nullableとnon-nullの型(「?」有りと無し)が分離
- コンストラクタのプロパティ(引数)にデフォルト値が指定可能
「JSONの記述⇔データクラスのオブジェクト」といった相互変換において、この違いに対応した処理が必要です。
この処理を実現する記述方法は2通りあります。「KotlinJsonAdapterFactory」と「Kotlin Codegen」です。
方法1:KotlinJsonAdapterFactoryKotlinJsonAdapterFactoryは、リフレクションでプロパティ値を注入する際に、上記の違いに対応したアダプターを提供します。
アダプターとはJsonAdapterクラスのことです。
JsonAdapterクラスは#fromJson()と#toJson関数を持ち、ここに変換の方法が記述されています。つまり、相互変換で呼び出される関数が記述されているクラスです。
public abstract class JsonAdapter<T> { public JsonAdapter() { } ... public abstract T fromJson(JsonReader var1) throws IOException; ... public abstract void toJson(JsonWriter var1, @Nullable T var2) throws IOException; ... }
どちらの方法も、「Kotlinの違いに対応したアダプター」を提供する点は同じです。ですので、どちらか一方の方法を用いれは良いです。
ただし、変換速度を考えるとKotlin Codegenの方が速いです。これはKotlin Serializationで使われるリフレクションが遅いためです。
方法1:KotlinJsonAdapterFactory
KotlinJsonAdapterFactoryを使用した方法です。
// @JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String // JSONのキーに別名を定義 )
MoshiはMoshi#adapter(Class型)で、引数に指定したクラスのJsonAdapterを取得します。このJsonAdapterはKotlinJsonAdapterFactoryから作成(ファクトリーのcreate関数を実行)されたものです。
JSON変換⇒データクラス
JsonAdapter#fromJsonを呼び出します。
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager" } """.trimIndent() val _moshi = Moshi.Builder() .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) // JsonAdapterを取得 val _oPerson = _adapter.fromJson(_jPerson) // データクラスのオブジェクトへ変換 Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager)
データクラス⇒JSON変換
JsonAdapter#toJsonを呼び出します。
val _moshi = Moshi.Builder() .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) // JsonAdapterを取得 val _oPerson = Person(id = 2, name = "Droid", post = "Staff") val _jPerson = _adapter.toJson(_oPerson) // Jsonの記述へ変換 Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff"}
注意:指定の順番
Moshi.Builder#add()を実行すれば、複数のJsonAdapter(もしくはファクトリー)を登録できます。
Moshiは変換対象のデータクラスのJsonAdapterを取得する際に、JsonAdapterを登録された物の中から登録された順に探します。
ドキュメントに「KotlinJsonAdapterFactoryは最後に指定、#addLast()を用いる」と書かれています。
これは、KotlinJsonAdapterFactoryが、自身の後に登録された物を無効(検索を打ち切る?)にするからです。ですから、最後に登録する必要があります。
... val _moshi = Moshi.Builder() .add(AAAJsonAdapterFactory()) // 有効 .add(BBBJsonAdapper()) // 有効 .add(KotlinJsonAdapterFactory()) .build() ...
... val _moshi = Moshi.Builder() .add(AAAJsonAdapterFactory()) // 有効 .add(KotlinJsonAdapterFactory()) .add(BBBJsonAdapper()) // 無効 .build() ...
#add()と同等な機能を有する#addLast()は、「最後であることを明確にするため」に用意されたものと思われます。
ただし、「#addLast()は、順番に関係なく、最後に登録したと見なされる」ことにならないので、注意が必要です。※Lastなのに、ちょっと、役立たずな関数…
... val _moshi = Moshi.Builder() .add(AAAJsonAdapterFactory()) // 有効 .add(BBBJsonAdapper()) // 有効 .addLast(KotlinJsonAdapterFactory()) // 最後であることを明確に! .build() ...
方法2:Kotlin Codegen
Android Studioはビルドをする際に、@JesonClass付きデータクラスを認識してJsonAdapterクラス(例:PersionJsonAdapter)を自動生成します。
@JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String // JSONのキーに別名を定義 )
※JsonAdapterクラスは「app(モジュール)/build/generated/ksp/…」以下に作成
生成されたJsonAdapterクラスは、Kotlinの違いに対応したアダプターです。
public class PersonJsonAdapter( moshi: Moshi, ) : JsonAdapter<Person>() { private val options: JsonReader.Options = JsonReader.Options.of("id", "name", "position") private val intAdapter: JsonAdapter<Int> = moshi.adapter(Int::class.java, emptySet(), "id") private val stringAdapter: JsonAdapter<String> = moshi.adapter(String::class.java, emptySet(),"name") public override fun toString(): String = buildString(28) { append("GeneratedJsonAdapter(").append("Person").append(')') } public override fun fromJson(reader: JsonReader): Person { var id: Int? = null var name: String? = null var post: String? = null reader.beginObject() while (reader.hasNext()) { when (reader.selectName(options)) { 0 -> id = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull("id", "id", reader) 1 -> name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("name", "name",reader) 2 -> post = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull("post", "position",reader) -1 -> { // Unknown name, skip it. reader.skipName() reader.skipValue() } } } reader.endObject() return Person( id = id ?: throw Util.missingProperty("id", "id", reader), name = name ?: throw Util.missingProperty("name", "name", reader), post = post ?: throw Util.missingProperty("post", "position", reader) ) } public override fun toJson(writer: JsonWriter, value_: Person?): Unit { if (value_ == null) { throw NullPointerException("value_ was null! Wrap in .nullSafe() to write nullable values.") } writer.beginObject() writer.name("id") intAdapter.toJson(writer, value_.id) writer.name("name") stringAdapter.toJson(writer, value_.name) writer.name("position") stringAdapter.toJson(writer, value_.post) writer.endObject() } }
MoshiはMoshi#adapter(Class型)で、引数に指定したクラスのJsonAdapterを取得します。このJsonAdapterは自動作成されたJsonAdapter(PersonJsonAdapter)です。
JSON⇒データクラス変換
JsonAdapter#fromJsonを呼び出します。
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager" } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) // PersonJsonAdapterを得る val _oPerson = _adapter.fromJson(_jPerson) // データクラスのオブジェクトへ変換 Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager)
データクラス⇒JSON変換
JsonAdapter#toJsonを呼び出します。
val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) // PersonJsonAdapterを得る val _oPerson = Person(id = 2, name = "Droid", post = "Staff") val _jPerson = _adapter.toJson(_oPerson) // Jsonの記述へ変換 Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff"}
例:Array
Array型のプロパティを持つ場合の相互変換です。
@JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String, // JSONのキーに別名を定義 val times: Array<Int> // Array型 )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":[32122, 43290, 46662, 68756] } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = _adapter.fromJson(_jPerson) Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager, times=[32122, 43290, 46662, 68756])データクラス⇒JSON変換
val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = arrayOf(28207, 43320, 46750, 62159) ) val _jPerson = _adapter.toJson(_oPerson) Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":[28207,43320,46750,62159]}
例:List
List型のプロパティを持つ場合の相互変換です。
@JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String, // JSONのキーに別名を定義 val times: List<Int> // List型 )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":[32122, 43290, 46662, 68756] } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = _adapter.fromJson(_jPerson) Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager, times=[32122, 43290, 46662, 68756])データクラス⇒JSON変換
val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = listOf(28207, 43320, 46750, 62159) ) val _jPerson = _adapter.toJson(_oPerson) Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":[28207,43320,46750,62159]}
例:Map
Mapのプロパティを持つ場合の相互変換です。
@JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String, // JSONのキーに別名を定義 val times: Map<Int, String> // Map型 )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":{"32122":"login","43290":"pause", "46662":"resume", "68756":"logout"} } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = _adapter.fromJson(_jPerson) Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager, times={32122=login, 43290=pause, 46662=resume, 68756=logout})データクラス⇒JSON変換
val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) 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 = _adapter.toJson(_oPerson) Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":{"28207":"login","43320":"pause","46750":"resume","62159":"login"}}
例:Set
Set型のプロパティを持つ場合の相互変換です。
@JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String, // JSONのキーに別名を定義 val times: Set<Int> // Set型 )
fun json2obj_Set() { val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":[32122, 43290, 46662, 68756] } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = _adapter.fromJson(_jPerson) Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager, times=[32122, 43290, 46662, 68756])データクラス⇒JSON変換
val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = setOf(28207, 43320, 46750, 62159) ) val _jPerson = _adapter.toJson(_oPerson) Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":[28207,43320,46750,62159]}
例:Enum
Enum型のプロパティを持つ場合の相互変換です。
enum class TimeSignal { LOGIN, LOGOUT, PAUSE, RESUME } @JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String, // JSONのキーに別名を定義 val times: TimeSignal // Enum型 )
fun json2obj_Enum() { val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":"LOGIN" } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = _adapter.fromJson(_jPerson) Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person6(id=1, name=Android, post=Manager, times=LOGIN)データクラス⇒JSON変換
fun obj2json_Enum() { val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = Person(id = 2, name = "Droid", post = "Staff", times = TimeSignal.LOGOUT) val _jPerson = _adapter.toJson(_oPerson) Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":"LOGOUT"}
例:データクラス
データクラス型のプロパティを持つ場合の相互変換です。
Kotlin Codegenの場合は、プロパティのデータクラス型(StampTime)に対しても、@JsonClassが必要になります。
@JsonClass(generateAdapter = true) data class StampTime(val hour: Int, val min: Int, val sec: Int) @JsonClass(generateAdapter = true) data class Person( val id: Int, val name: String, @Json(name = "position") val post: String, // JSONのキーに別名を定義 val times: StampTime // データクラス )
val _jPerson = """ { "id":1, "name":"Android", "position":"Manager", "times":{"hour":8,"min":55,"sec":22} } """.trimIndent() val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = _adapter.fromJson(_jPerson) Log.i(TAG, "(Moshi) Person Obj = ${_oPerson}")
(Moshi) Person Obj = Person(id=1, name=Android, post=Manager, times=StampTime(hour=8, min=55, sec=22))データクラス⇒JSON変換
val _moshi = Moshi.Builder() // .addLast(KotlinJsonAdapterFactory()) .build() val _adapter = _moshi.adapter(Person::class.java) val _oPerson = Person( id = 2, name = "Droid", post = "Staff", times = StampTime(7, 50, 7) ) val _jPerson = _adapter.toJson(_oPerson) Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":{"hour":7,"min":50,"sec":7}}
nullable/non-nullとデフォルト値の対応
Moshiにおいて、nullable/non-nullとデフォルト値の対応は表のようになります。
JSON⇒データクラス変換JSONの記述で欠けている「Name:Value」は、Nullとして扱われます。
JSONの記述 | ⇒ | データクラスの定義 | オブジェクト |
---|---|---|---|
{ "id":1, "name":"Android" } | (変換) | data class Person0( val id: Int, val name: String, val post: String ) | JsonDataException |
data class Person0( val id: Int, val name: String, val post: String? ) | id = 1 name = Android post = null |
||
data class Person0( val id: Int, val name: String, val post: String = ”Staff” ) | id = 1 name = Android post = Staff |
||
data class Person0( val id: Int, val name: String, val post: String? = null ) | id = 1 name = Android post = null |
||
{ "id":1, "name":"Android", "post":null } | data class Person0( val id: Int, val name: String, val post: String ) | JsonDataException | |
data class Person0( val id: Int, val name: String, val post: String? ) | id = 1 name = Android post = null |
値がNullのプロパティは、JSONの記述に出力されません。
データクラスの定義 | オブジェクト | ⇒ | JSONの記述 |
---|---|---|---|
data class Person0( val id: Int, val name: String, val post: String? ) | id = 2 name = Droid post = null | (変換) | { "id":2, "name":"Droid" } |
関連記事: