ユーザ定義クラスのカスタムSerializer(Handwritten版)

投稿日:  更新日:

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

例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。

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

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

カスタムSerializer(ユーザ定義クラス、Handwritten版)の作成方法をまとめます。

※環境:Android Studio Meerkat | 2024.3.1
    Kotlin 2.0.0
    Kotlin serialization json 1.7.1

スポンサーリンク

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

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

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

上記以外の一般クラス(ユーザ定義のクラス、ライブラリ提供のクラス)は未対応です。

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

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

    override fun toString() = "%s:%s:%s".format(hh, mm, ss)
}

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

スポンサーリンク

Serializerの種類

Serializerは、表のような種類(書き方)があります。

特徴シリアル化の構成
PrimitivePrimitive型(Stringを含む)限定PrimitiveSerialDescriptor
Delegating定義済みのSerializerへ委任するSerialDescriptor
Surrogate代理クラスを立て(作成し)、
そのクラスのSerializerに代行させる
SerialDescriptor
Handwritten全ての要素のシリアル化を細かく定義buildClassSerialDescriptor

この記事で取り上げるのは、Handwritten版です。
※種類は「Custom serializers」に紹介されています。

スポンサーリンク

カスタムSerializerの作成

Serializer(KSerializerインターフェース)は「Kotlin serializerがJSONをパースする際に呼び出される関数」を持ちます。

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()
    }
}
 KSerializer#serialize( ) 

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

 KSerializer#deserialize( ) 

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

ですので、カスタムSerializerは、このKSerializerインターフェースを継承して、相互変換の方法をserialiseとdesirializeへ実装します。

object LogTimeHandwrittenSerializer : KSerializer<LogTime> {

    override val descriptor: SerialDescriptor =
            buildClassSerialDescriptor("com.example.LogTimeHandwriteSerializer") {
                element<Int>("hh")		// index:0を割り当て
                element<Int>("mm")		// index:1を割り当て
                element<Int>("ss")		// index:2を割り当て
            }

    // Obj->JSONの変換
    override fun serialize(encoder: Encoder, value: LogTime) {
        encoder.encodeStructure(descriptor) {
            encodeIntElement(descriptor, 0, value.hh)
            encodeIntElement(descriptor, 1, value.mm)
            encodeIntElement(descriptor, 2, value.ss)
        }
    }

    // JSON->Objの変換
    override fun deserialize(decoder: Decoder): LogTime {
        return decoder.decodeStructure(descriptor) {
            var _hh = 0
            var _mm = 0
            var _ss = 0
            while(true) {
                when(val _index = decodeElementIndex(descriptor)) {
                    0 -> _hh = decodeIntElement(descriptor, 0)
                    1 -> _mm = decodeIntElement(descriptor, 1)
                    2 -> _ss = decodeIntElement(descriptor, 2)
                    CompositeDecoder.DECODE_DONE -> break      // elementの終わり
                    else -> error("Unexpected index: ${_index}")
                }
            }
            LogTime(_hh, _mm, _ss)
        }
    }
}
スポンサーリンク

SerialDescriptor(シリアル化の構成)

SerialDescriptorでシリアル化の構成を定義しています。つまり、「JSON上でどのように表現するか!」です。

Handwritten(手書き)版の場合、プログラマが全ての要素を細かく定義しなければなりません。

定義にbuildClassSerialDescriptor( )を使います。

    override val descriptor: SerialDescriptor =		// ↓↓ 
            buildClassSerialDescriptor("com.example.LogTimeHandwriteSerializer") {
                element<Int>("hh")		// index:0を割り当て
                element<Int>("mm")		// index:1を割り当て
                element<Int>("ss")		// index:2を割り当て
            }

elementは対象クラスの持つ要素を表しています。

要素は3つあり、それぞれのkey名が「hh,mm,ss」で、value値の型がIntであることを表します。ここで、elementの並ぶ順番に、indexが割り当てられます。

JSONの要素の並ぶ順番は任意です。順番は「mm,ss,hh」でも「hh,ss,mm」でも問題ありません。

deserializeにおいて、順番の違いへ対応するために、indexが大きな役割を果たします。

スポンサーリンク

deserialize(JSON⇒Objの変換方法)

シリアル化されたデータ(JSONの記述)は、全てのデータが連結されてリボンの様になっており、先頭から順番に送られてきます。

変換は、このリボン上を「なぞる」ように行う必要があります。要素の処理を行う毎に、ポインター(処理を行う点)が移動すると考えれば良いです。

変換はこのリボン上を「なぞる」ように行う

図のように、indexを取り出すdecodeElementIndexとvalueを取り出すdecodeIntElementが対になります。

    override fun deserialize(decoder: Decoder): LogTime {
        return decoder.decodeStructure(descriptor) {
            var _hh = 0
            var _mm = 0
            var _ss = 0
            while(true) {
                when(val _index = decodeElementIndex(descriptor)) {
                    0 -> _hh = decodeIntElement(descriptor, 0)
                    1 -> _mm = decodeIntElement(descriptor, 1)
                    2 -> _ss = decodeIntElement(descriptor, 2)
                    CompositeDecoder.DECODE_DONE -> break      // elementの終わり
                    else -> error("Unexpected index: ${_index}")
                }
            }
            LogTime(_hh, _mm, _ss)
        }
    }

whenを用いてindex毎に制御を分岐させているのは、先に述べた「JSONの要素の並ぶ順番は任意」のためです。

このようにしておけば、要素の順番が入れ代わっていても、問題ありません。

要素の順番

ちなみに、「順番が一意、要素数が3固定」であると保証されれば、whenは省略できます。

    override fun deserialize(decoder: Decoder): LogTime {
        return decoder.decodeStructure(descriptor) {
            decodeElementIndex(descriptor)
            val _hh = decodeIntElement(descriptor, 0)
            decodeElementIndex(descriptor)
            val _mm = decodeIntElement(descriptor, 1)
            decodeElementIndex(descriptor)
            val _ss = decodeIntElement(descriptor, 2)
            LogTime(_hh, _mm, _ss)
        }
    }
スポンサーリンク

serialize(Obj⇒JSONの変換方法)

SerialDescriptorで定義されたindexの番号を使って、要素のシリアル出力を並べます。

並ぶ順番はencodeIntElementの記述順です。

    override fun serialize(encoder: Encoder, value: LogTime) {
        encoder.encodeStructure(descriptor) {
            encodeIntElement(descriptor, 0, value.hh) // index:0は「"hh":value」
            encodeIntElement(descriptor, 1, value.mm) // index:1は「"mm":value」
            encodeIntElement(descriptor, 2, value.ss) // index:2は「"ss":value」
        }
    }

変換

ちなみに、「JSONの要素の並ぶ順番は任意」なので、順番を変えても問題ありません。

    override fun serialize(encoder: Encoder, value: LogTime) {
        encoder.encodeStructure(descriptor) {
            encodeIntElement(descriptor, 1, value.mm) // index:1は「"mm":value」
            encodeIntElement(descriptor, 2, value.ss) // index:2は「"ss":value」
            encodeIntElement(descriptor, 0, value.hh) // index:0は「"hh":value」
        }
    }

要素の順番は任意

スポンサーリンク

カスタムSerializerの使用

カスタムSerializerはアノテーション@Serializableの引数で登録します。

Android Studioはビルドをする際に、@Serializable付きのデータクラスを認識して、データクラスのJavaバイナリへ、引数のカスタムSerializerクラスを埋め込みます。

@Serializable(LogTimeHandwrittenSerializer::class)
class LogTime(hour: Int, min: Int, sec: Int) {
    val hh: Int = hour
    val mm: Int = min
    val ss: Int = sec

    override fun toString() = "%s:%s:%s".format(hh, mm, ss)
}

@Serializable
data class Person(
    val id: Int,
    val name: String,
    @SerialName("position") val post: String,  // JSONのキーに別名を定義
    val times: LogTime
)

Json⇒Obj変換

Json#decodeFromStringを呼び出します。

内部で対象クラスのdeserialize( )が呼び出されます。

    val _jPerson = """
            {
                "id":1,
                "name":"Android",
                "position":"Manager",
                "times":{"hh":8,"mm":55,"ss":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=8:55:22)

Obj⇒JSON変換

Json#encodeToStringを呼び出します。

内部で対象クラスのserialize( )が呼び出されます。

    val _oPerson = Person(
        id = 2, name = "Droid", post = "Staff",
        times = LogTime(12, 2, 0)
    )
    val _jPerson = Json.encodeToString(_oPerson)

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

Exceptionのメッセージ、オフセットとは

以下のdeserializeは、「index:1」で処理を打ち切ったため、Exceptionが発生します。

    override fun deserialize(decoder: Decoder): LogTime {
        return decoder.decodeStructure(descriptor) {
            var _hh = 0
            var _mm = 0
            var _ss = 0
            decodeElementIndex(descriptor)
            _hh = decodeIntElement(descriptor, 0)
            decodeElementIndex(descriptor)
            _mm = decodeIntElement(descriptor, 1)
//            decodeElementIndex(descriptor)		// 原因:index:2が未処理で終了
//            _ss = decodeIntElement(descriptor, 2)
            LogTime(_hh, _mm, _ss)
        }
    }

この時、次のようなメッセージが出力されます。

kotlinx.serialization.json.internal.JsonDecodingException: 
  Unexpected JSON token at offset 69: Trailing comma before the end of JSON  at path: $.times.mm
Trailing commas are non-complaint JSON and not allowed by default. 
Use 'allowTrailingCommas = true' in 'Json {}' builder to support them.
JSON input: {"id":1,"name":"Android","position":"Manager","times":{"hh":8,"mm":55,"ss":22}}
---
オフセット69に予期しないJSONトークン。 JSONの末尾にコンマがあります。
末尾のコンマは非準拠のJSONであり、デフォルトでは許可されません。

メッセージに登場する「offset XX」は、シリアル化データ(JSONの記述)の先頭らからの文字数です。

オフセットの定義

シリアル化データ(JSONの記述)に空白や制御文字(\n,\tなど)があれば、それらの文字もoffsetに含まれるので注意して下さい。

スポンサーリンク

関連記事:

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で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 ...
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でJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ユーザ定義クラス、Primitive版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1     Kotlin 2.0.0     Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ライブラリ提供クラス、Primitive版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1     Kotlin 2.0.0     Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ユーザ定義クラス、Delegating版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1     Kotlin 2.0.0     Kotlin serialization json 1.7.1 ...
Kotlin serializationでJSONをパース(JSONの記述⇔クラスのオブジェクト)する場合に、一般クラスは未対応です。 例えば、データクラス(コンストラクタの引数でプロパティを指定するクラス)以外の、ユーザ定義のクラスはパース出来ません。また、ライブラリ提供のクラスもパース出来ません。 対応させるためには、そのクラスのカスタムSerializerを作成します。 そして、相互変換する方法をプログラマー側で定義します。 カスタムSerializer(ライブラリ提供クラス、Surrogate版)の作成方法をまとめます。 ※環境:Android Studio Meerkat | 2024.3.1     Kotlin 2.0.0     Kotlin serialization json 1.7.1 ...
スポンサーリンク