MoshiのカスタムJsonAdaperを作成

投稿日:  更新日:

MoshiでJSONをパース(JSONの記述⇔データクラスのオブジェクト)する場合に、一般クラスは未対応です。

例えば、データクラス以外の、ユーザ定義のクラスはパース出来ません。

対応させるためには、そのクラスのカスタムJsonAdapterを作成します。

そして、相互変換する方法をプログラマー側で定義します。

カスタムJsonAdapterの作成方法をまとめます。

※環境:Android Studio Ladybug Feature Drop | 2024.2.2
    Kotlin 2.0.0
    Moshi 1.15.2

スポンサーリンク

標準で対応する型、しない型

MoshiでJSONをパース(JSONの記述⇔データクラスのオブジェクト)する場合に、標準で対応する型は次の通りです。

  • ・Int,Long,Float,Double,Booleanなど(プリミティブ型)
  • ・String(文字列)
  • ・Array(配列)
  • ・List,Map,Set(コレクション型)
  • ・Enum(列挙型)
  • ・データクラス

上記以外の、一般クラス(ユーザ定義のクラスを含む)は未対応です。

例えば、次のようなクラスは標準でパース出来ません。

class TimeLog(hour: Int, min: Int, sec: Int) {
    val hh: Int = hour
    val mm: Int = min
    val ss: Int = sec

    override fun toString(): String {
        return "%s:%s:%S".format(hh, mm, ss)
    }
}

一般クラスをパースするには、そのクラスのカスタムJsonAdapterが必要です。

スポンサーリンク

カスタムJsonAdapterの作成

JsonAdaperは「MoshiがJSONをパースする際に呼び出される関数」を持ちます。

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;
	...
}
 JsonAdapter#fromJson( ) 

「JSONの記述⇒データクラスのオブジェクト」で呼び出し

 JsonAdapter#toJson( ) 

「JSONの記述⇐データクラスのオブジェクト」で呼び出し

ですので、カスタムJsonAdapterは、この抽象クラスJsonAdapterを継承して、相互変換の方法を#fomJsonと#toJsonへ実装します。

class LogTimeJsonAdapter: JsonAdapter<TimeLog>() {

    override fun fromJson(reader: JsonReader): TimeLog  {
        val _hhmmss = reader.nextString().split(":")
        val _hh = _hhmmss[0].toInt()
        val _mm = _hhmmss[1].toInt()
        val _ss = _hhmmss[2].toInt()
        return TimeLog(_hh, _mm, _ss)
    }

    override fun toJson(writer: JsonWriter, value: TimeLog?) {
        writer.value("%s:%s:%s".format(value?.hh, value?.mm, value?.ss))
    }
}
スポンサーリンク

カスタムJsonAdapterの使用

カスタムJsonAdapterはMoshi.Builder#add( )で登録します。

相互変換を行う際に、アダプターは登録された中から検索されます。そして、対象のアダプターがあれば使用されます。

※MoshiでJSONをパースする方法については「MoshiでJSONをパース」を参照

class TimeLog(hour: Int, min: Int, sec: Int) {
    val hh: Int = hour
    val mm: Int = min
    val ss: Int = sec

    override fun toString(): String {
        return "%s:%s:%S".format(hh, mm, ss)
    }
}

@JsonClass(generateAdapter = true)
data class Person(
    val id: Int,
    val name: String,
    @Json(name = "position") val post: String,  // JSONのキーに別名を定義
    val times: TimeLog
)

JSON⇒データクラス変換

JsonAdapter#fromJsonを呼び出します。

    val _jPerson = """
            {
                "id":1,
                "name":"Android",
                "position":"Manager",
                "times":"8:55:22"
            }
        """.trimIndent()
    val _moshi = Moshi.Builder()
        .add(LogTime::class.java, LogTimeJsonAdapter())
        .build()
    val _adapter = _moshi.adapter(Person::class.java)
    val _oPerson = _adapter.fromJson(_jPerson)

    Log.i(TAG, "(Moshi) Person Obj  = ${_oPerson}")
(Moshi) Person Obj  = Person8(id=1, name=Android, post=Manager, times=8:55:22)

データクラス⇒JSON変換

JsonAdapter#toJsonを呼び出します。

    val _moshi = Moshi.Builder()
        .add(LogTime::class.java, LogTimeJsonAdapter())
        .build()
    val _adapter = _moshi.adapter(Person::class.java)
    val _oPerson = Person(
        id = 2, name = "Droid", post = "Staff",
        times = LogTime(12, 2, 0)
    )
    val _jPerson = _adapter.toJson(_oPerson)

    Log.i(TAG, "(Moshi) Person Json = ${_jPerson}")
(Moshi) Person Json = {"id":2,"name":"Droid","position":"Staff","times":"12:2:0"}
スポンサーリンク

アダプターのファクトリー化

Moshi.Builder#add( )でJsonAdapterを登録する際に、ファクトリークラスで登録することが出来ます。

アダプターをファクトリー化するために、JsonAdapter.Factoryインターフェースが用意されています。

public abstract class JsonAdapter<T> {
	...
	
    public interface Factory {
        @CheckReturnValue
        @Nullable
        JsonAdapter<?> create(Type var1, Set<? extends Annotation> var2, Moshi var3);
    }
}

このインターフェースを継承して、ファクトリークラスを作成します。

専用ファクトリー

カスタムJsonAdapter専用のファクトリークラスです。

val LogTimeJsonAdapterFactory = LogTimeJsonAdapter.FACTORY

class LogTimeJsonAdapter: JsonAdapter<LogTime>() {

    companion object {
        val FACTORY: Factory = object: Factory {
            override fun create(
                type: Type, annotations: Set<out Annotation>, moshi: Moshi
            ): JsonAdapter<*>? {
                if (type === LogTime::class.java) return LogTimeJsonAdapter()
                return null
            }
        }
    }

    override fun fromJson(reader: JsonReader): LogTime  {
        val _hhmmss = reader.nextString().split(":")
        val _hh = _hhmmss[0].toInt()
        val _mm = _hhmmss[1].toInt()
        val _ss = _hhmmss[2].toInt()
        return LogTime(_hh, _mm, _ss)
    }

    override fun toJson(writer: JsonWriter, value: LogTime?) {
        writer.value("%s:%s:%s".format(value?.hh, value?.mm, value?.ss))
    }
}

Moshiはアダプターを検索する際にファクトリーを見つけると、Factory#create( )を実行して、目的のアダプターを作成しようとします。作成できれば、そのアダプターを使います。

    val _jPerson = """
            {
                "id":1,
                "name":"Android",
                "position":"Manager",
                "times":"8:55:22"
            }
        """.trimIndent()
    val _moshi = Moshi.Builder()
        .add(LogTimeJsonAdapterFactory)
        .build()
    val _adapter = _moshi.adapter(Person::class.java)
    val _oPerson = _adapter.fromJson(_jPerson)

    Log.i(TAG, "(Moshi) Person Obj  = ${_oPerson}")
(Moshi) Person Obj  = Person8(id=1, name=Android, post=Manager, times=8:55:22)
スポンサーリンク

複数ファクトリー

複数のカスタムJsonAdapterをまとめたファクトリークラスです。

object UserJsonAdaptersFactory : JsonAdapter.Factory {
    
    override fun create(
        type: Type, anotations: Set<Annotation?>, moshi: Moshi
    ): JsonAdapter<*>? {
        if (type === LogTime::class.java) return LOGTIME_JSON_ADAPTER
        // 同様に複数のAdapterを作成可能
        //   Adapter XXXの作成
        //   Adapter YYYの作成
        //   Adapter ZZZの作成
        return null
    }
    
    val LOGTIME_JSON_ADAPTER = object : JsonAdapter<LogTime>() {
        override fun fromJson(reader: JsonReader): LogTime {
            val _hhmmss = reader.nextString().split(":")
            val _hh = _hhmmss[0].toInt()
            val _mm = _hhmmss[1].toInt()
            val _ss = _hhmmss[2].toInt()
            return LogTime(_hh, _mm, _ss)
        }
        override fun toJson(writer: JsonWriter, value: LogTime?) {
            writer.value("%s:%s:%s".format(value?.hh, value?.mm, value?.ss))
        }
    }
    // ここに、複数のAdapterを記述可能
    //   Adapter XXXの記述
    //   Adapter YYYの記述
    //   Adapter ZZZの記述
}

Moshiはアダプターを検索する際にファクトリーを見つけると、Factory#create( )を実行して、目的のアダプターを作成しようとします。作成できれば、そのアダプターを使います。

    val _jPerson = """
            {
                "id":1,
                "name":"Android",
                "position":"Manager",
                "times":"8:55:22"
            }
        """.trimIndent()
    val _moshi = Moshi.Builder()
        .add(UserJsonAdaptersFactory)
        .build()
    val _adapter = _moshi.adapter(Person::class.java)
    val _oPerson = _adapter.fromJson(_jPerson)

    Log.i(TAG, "(Moshi) Person Obj  = ${_oPerson}")
(Moshi) Person Obj  = Person8(id=1, name=Android, post=Manager, times=8:55:22)
スポンサーリンク

関連記事:

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 ...
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 ...
スポンサーリンク