MoshiでJSONをパース

投稿日:  更新日:

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の記述⇔データクラスのオブジェクト」といった相互変換(パース:解析して変換)が簡単に出来ます。

Moshiに出来ること

プログラム間(コンピュータ間や異なるプログラミング言語間など)でデータを受け渡す際に、データのフォーマットとしてJSONが広く使われています。

その一例は、Retrofit(REST準拠のAPI)です。

RetrofitでMoshiを利用すれば、HTTPプロトコルでサーバーにデータを要求し、返答をJSONフォーマットで受け取り、データクラスのオブジェクトでプログラムへ取り込むことが可能です。

Moshiの使用事例(Retrofitの場合)

とても、便利です。

なお、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:KotlinJsonAdapterFactory 
Moshiはデータクラスのオブジェクトをリフレクションで作成します。

KotlinJsonAdapterFactoryは、リフレクションでプロパティ値を注入する際に、上記の違いに対応したアダプターを提供します。

 方法2:Kotlin Codegen 
Kotlin Codegenは、上記の違いに対応したカスタムアダプターを自動作成し、提供します。

アダプターとは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のキーに別名を定義
)

MoshiのJsonAdapter作成フロー
※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型
)
 JSON⇒データクラス変換 
    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型
)
 JSON⇒データクラス変換 
    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型
)
 JSON⇒データクラス変換 
    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型
)
 JSON⇒データクラス変換 
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型
)
 JSON⇒データクラス変換 
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						// データクラス
)
 JSON⇒データクラス変換 
    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
 データクラス⇒JSON変換 

値が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"
}
スポンサーリンク

関連記事:

MoshiでJSONをパース(JSONの記述⇔データクラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス以外の、ユーザ定義のクラスはパース出来ません。 対応させるためには、そのクラスのカスタムJsonAdapterを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムJsonAdapterの作成方法をまとめます。 ※環境:Android Studio Ladybug Feature Drop | 2024.2.2     Kotlin 2.0.0     Moshi 1.15.2 ...
RetrofitはRESTに準拠したWeb APIです。 このRetrofitとMoshiを使って、Webサービスへアクセスする方法を、まとめました。 サンプルはAndroidのコードラボと同じ内容です。私はコードラボが理解し難かったので、不要な部分をそぎ落として、Retrofitに的を絞って説明しています。 ※環境:Android Studio Ladybug Feature Drop | 2024.2.2     Kotlin 2.0.0     Moshi 1.15.2     Retrofit2 2.11.0     Coil 3.1.0 ...
スポンサーリンク