jackon-module-kotlinでvalue class(inline class)をシリアライズする

`jackson-moduole-kotlin`の`value class`に関するシリアライズサポートは2.15にて大幅に強化されました（しました）。
`value class`のシリアライズサポートが必要であれば、まずバージョンアップを検討するようお願いします。

https://github.com/FasterXML/jackson-module-kotlin/blob/2.16/release-notes/VERSION-2.x#L31

また、`jackson-module-kotlin`に関する実験的プロジェクトも有ります。
プロダクションコードでの利用は推奨しませんが、こちらでは`value class`のデシリアライズ、パラメータに付与されたアノテーションの有効化といった機能が利用できます。

https://github.com/ProjectMapK/jackson-module-kogera/blob/develop/docs/FixedIssues.md

---

この記事は`Swift/Kotlin愛好会 Advent Calendar 2021`の8日目の記事になりました。

https://qiita.com/advent-calendar/2021/love_swift_kotlin

# jackson-module-kotlinでのvalue classのシリアライズについて
`jackson-module-kotlin`では、`2.13.0`現在`value class`/`inline class`[^inline-class]のシリアライズに以下のような不具合が有ります。

[^inline-class]: 以後は現在の名称である`value class`に合わせます。

- シリアライズ結果が異常な値になる場合がある
- カスタムシリアライザが機能しない場合がある
- `JsonSerialize`や`JsonValue`を使ってのシリアライズ方法指定が機能しない

この記事では`2.13.0`以前の`jackson-module-kotlin`でも`value class`を正常にシリアライズする方法について説明します。
技術的な解説については非常に長くなってしまったため別記事として公開~~予定です~~しました。

https://qiita.com/wrongwrong/items/ed3ba2aa108633abf452

なお、記事では`jackson-module-kotlin 2.13.0`/`Kotlin 1.5.0`を前提にしています。
また、簡単のため`kotlin-reflect`も利用します。

**用語に関する補足**
この記事の中では、`value class`からプロパティの値を取り出すことを「`unbox`する」、`value class`のインスタンスを生成することを「`box`する」と表現します。

ここで、「`unbox`する」とした場合、取り出される値は必ず`value class`の`primary constructor`の引数と同じになる点にはご注意下さい。
例えば以下のような`value class`が有った場合、`Kotlin`からは`jsonValue`プロパティにしかアクセスできませんが、`unbox`した際に取り出されるのは`value`プロパティの値になります。

```kotlin
@JvmInline
value class Value(private val value: Int) {
    @get:JsonValue
    val jsonValue get() = "JsonValue $value"
}
```

**挙動に関する補足**
`jackson-module-kotlin`では、`value class`のシリアライズ結果について、指定が無ければ`unbox`された値のシリアライズ結果に合わせる方針となっています。
一方、そうなっていない場面もいくつかあったため、バージョンアップ毎に段階的に修正が行われている状況です。
このため、利用するバージョンによってはこの記事で記載する内容と異なる挙動になる可能性が有る点にご注意下さい。

なお、`Java Reflection`から見た`value class`に関しては以前作成した資料が有りますので、よろしければそちらもご参照ください。

https://speakerdeck.com/k163377/java-reflectionkarajian-tavalue-class

# やること
以下に示す、`value class`をプロパティに持つ`Target`クラスをシリアライズするとします。

```kotlin
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.StdSerializer

data class Target(
    // ULongとしてシリアライズしたい（※unsigned integersはvalue classとして定義されている）
    val foo: ULong = ULong.MAX_VALUE,
    // ObjectMapperに登録したカスタムシリアライザでシリアライズしたい　
    val bar: V1 = V1(1),
    // JsonSerializeに設定したシリアライザでシリアライズしたい（この状態ではシリアライズが失敗するため一旦コメントアウト）
    // @get:JsonSerialize(using = V2.CustomSerializer::class)
    val baz: V2 = V2(2),
    // JsonValueでシリアライズしたい
    val qux: V3 = V3(3),
    // 中身の値をunboxした形にシリアライズしたい（通常挙動の動作を変えていないことの確認）
    val quux: V4 = V4(4)
)

@JvmInline
value class V1(val value: Int) {
    object CustomSerializer : StdSerializer<V1>(V1::class.java) {
        override fun serialize(value: V1, gen: JsonGenerator, provider: SerializerProvider) {
            gen.writeString("CustomSerializer ${value.value}")
        }
    }
}

@JvmInline
value class V2(val value: Int) {
    class CustomSerializer : StdSerializer<V2>(V2::class.java) {
        override fun serialize(value: V2, gen: JsonGenerator, provider: SerializerProvider) {
            gen.writeString("JsonSerializer ${value.value}")
        }
    }
}

@JvmInline
value class V3(val value: Int) {
    @get:JsonValue
    val jsonValue get() = "JsonValue $value"
}

@JvmInline
value class V4(val value: Int)

```

これをそのままシリアライズすると以下のようになります。

```kotlin:シリアライズするコード
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder

val simpleModule = SimpleModule()
    .addSerializer(V3.CustomSerializer)
val writer = jacksonMapperBuilder()
    .addModule(simpleModule)
    .build()
    .writerWithDefaultPrettyPrinter()

println(writer.writeValueAsString(Target()))
```

```json:実行結果
{
  "foo" : -1,
  "bar" : 1,
  "baz" : 2,
  "qux" : 3,
  "quux" : 4
}
```

ご覧の通り、`quux`（`unbox`した形にシリアライズしたい）プロパティ以外は正常にシリアライズされていません。
この記事では、`Target`クラスを期待通りの形にシリアライズする方法について説明します。

# やり方
`Target`クラスを期待通りの形にシリアライズするには幾つかの問題を解決する必要が有るため、その方法を順番に解説していきます。
また、今回紹介する方法はリフレクションを多く使うため、シリアライズ性能が低下する点にご注意ください。

## ULongをシリアライズする/ObjectMapperに登録したカスタムシリアライザを機能させる
まず、プロパティが`ULong`の場合と、`ObjectMapper`に登録したカスタムシリアライザで正常にシリアライズする方法を紹介します。

はじめに以下のようなシリアライザを定義します。
このシリアライザは`jackson`が処理する値の見え方を`Kotlin`に合わせるような効果が有ります。
ただし、このシリアライザを`Serializers`などで`ObjectMapper`に登録する方法は機能しない点にご注意ください。

```kotlin
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer

// outerClazzはvalue class、innerClazzはvalue classの中身の型
class ValueClassBoxSerializer<T : Any>(
    private val outerClazz: Class<out Any>, innerClazz: Class<T>
) : StdSerializer<T>(innerClazz) {
    private val boxMethod = outerClazz.getMethod("box-impl", innerClazz)

    // valueはvalue classの中身、nullの場合が有る
    override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) {
        // unboxされている値をvalue classにboxする
        // 元々constructor-implを経由した値だと思われるため、その呼び出しは省略してもよい
        val boxed: Any = boxMethod.invoke(null, value)
        // boxされた値に対するシリアライザを検索し、シリアライズする
        provider.findValueSerializer(outerClazz).serialize(boxed, gen, provider)
    }
}
```

次に、このシリアライザを機能させるための`AnnotationIntrospector`を定義します。

```kotlin
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter

object ValueClassSerializeAnnotationIntrospector : NopAnnotationIntrospector() {
    private fun findSerializerForValueClass(am: Annotated): Any? = if (am is AnnotatedMethod) {
        val getter = am.member.apply {
            // getterが直接value classを返している場合は何もしない
            if (this.returnType.isValue) return null
        }

        val kotlinProperty = getter.getKotlinProperty()
        val returnClazz = kotlinProperty?.returnType?.classifier as? KClass<*>

        // 取得したプロパティのKotlin上の戻り値がvalue classであれば、対応するシリアライザを返す
        returnClazz
            ?.takeIf { it.isValue }
            ?.java
            ?.let { outerClazz ->
                @Suppress("UNCHECKED_CAST")
                ValueClassBoxSerializer(outerClazz, getter.returnType)
            }
    } else {
        // value classはJvmFieldにできないため、getter以外のパターンは無視してよい
        null
    }

    override fun findSerializer(am: Annotated): Any? = findSerializerForValueClass(am)
    // value classの中身がnullだった場合、findNullSerializerで処理する必要がある
    override fun findNullSerializer(am: Annotated): Any? = findSerializerForValueClass(am)
}

// Classがvalue classかの判定
private val Class<*>.isValue: Boolean get() = this.kotlin.isValue
// Java GetterからKotlin Propertyへの変換
private fun Method.getKotlinProperty(): KProperty<*>? = this
    .declaringClass
    .kotlin
    .let {
        // memberPropertiesの取得はエッジケースで失敗することが有るため、エラーが出た場合は取得できなかったと見なす
        try { it.memberProperties } catch (e: Error) { null }
    }?.find { it.javaGetter == this }
```

最後に、この`AnnotationIntrospector`を以下のような`SimpleModule`経由で`ObjectMapper`に登録します。

```kotlin
val simpleModule = object : SimpleModule() {
    init { addSerializer(V1.CustomSerializer) }

    override fun setupModule(context: SetupContext) {
        super.setupModule(context)

        context.appendAnnotationIntrospector(ValueClassSerializeAnnotationIntrospector)
    }
}
```

すると、実行結果が以下のように変化します。
`ULong`と`ObjectMapper`に登録したカスタムシリアライザで正常にシリアライズできるようになっています。

```diff_json:実行結果
{
+ "foo" : 18446744073709551615,
- "foo" : -1,
+ "bar" : "CustomSerializer 1",
- "bar" : 1,
  "baz" : 2,
  "qux" : 3,
  "quux" : 4
}
```

## JsonSerializeに設定したシリアライザでシリアライズする
次に、`JsonSerialize`に設定したシリアライザでシリアライズする方法を紹介します。

これを機能させるためには、カスタムシリアライザ側で`unbox`された値を処理する形に修正する必要があります。

```diff_kotlin
@JvmInline
value class V2(val value: Int) {
+   class CustomSerializer : StdSerializer<Int>(Int::class.java) {
+       // valueにはunboxされた値が入力される
+       override fun serialize(value: Int, gen: JsonGenerator, provider: SerializerProvider) {
+           gen.writeString("JsonSerializer ${V2(value).value}")
-   class CustomSerializer : StdSerializer<V2>(V2::class.java) {
-       override fun serialize(value: V2, gen: JsonGenerator, provider: SerializerProvider) {
-           gen.writeString("JsonSerializer ${value.value}")
        }
    }
}
```

次に、`Target`クラスで`JsonSerializ`アノテーションに対して行っていたコメントアウトを解除します。

```diff_kotlin
    // JsonSerializeに設定したシリアライザでシリアライズしたい（この状態ではシリアライズが失敗するため一旦コメントアウト）
+   @get:JsonSerialize(using = V2.CustomSerializer::class)
-   // @get:JsonSerialize(using = V2.CustomSerializer::class)
```

この修正を行うと、実行結果が以下のように変化します。
`JsonSerialize`に設定したシリアライザで正常にシリアライズできるようになっています。

```diff_json:実行結果
{
  "foo" : 18446744073709551615,
  "bar" : "CustomSerializer 1",
+ "baz" : "JsonSerializer 2",
- "baz" : 2,
  "qux" : 3,
  "quux" : 4
}
```

### 注意点
`JsonSerialize`を用いる場合の注意点が2つ有ります。

1つ目は、`serialize`に入力される値についてです。
用語に関する補足で書いた通り、ここに入ってくる値は`primary constructor`の引数と同じになります。
このため、`Kotlin`上でアクセスできる値とは異なる値が入力となる場合が有ります。
例えば、プロパティの値が`UInt.MAX_VALUE`だった場合、`serialize`への入力は`-1`になります。

2つ目は、アノテーションの付与方法についてです。
`jackson-module-kotlin`では、通常`get:`無しでアノテーションを付与した場合も正常にシリアライズが行われます。
一方、`value class`に対しては`get:`を付与しなければ正常にシリアライズされません。

## JsonValueでシリアライズする
最後に、`JsonValue`でシリアライズする方法を紹介します。

はじめに、以下のようなシリアライザを定義します。
このシリアライザに関しても、`ObjectMapper`に登録する方法は機能しません。

```kotlin
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.lang.reflect.Method
import java.lang.reflect.Modifier

class ValueClassJsonValueSerializer<T> private constructor(
    innerClazz: Class<T>,
    private val jsonValueGetter: Method
) : StdSerializer<T>(innerClazz) {
    override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) {
        // ファクトリメソッドの通り、jsonValueGetterは必ずstaticメソッド
        val jsonValue: Any? = jsonValueGetter.invoke(null, value)
        jsonValue
            ?.let { provider.findValueSerializer(it::class.java).serialize(it, gen, provider) }
            ?: provider.findNullValueSerializer(null).serialize(null, gen, provider)
    }

    companion object {
        fun <T> createdOrNull(
            outerClazz: Class<out Any>,
            innerClazz: Class<T>
        ): ValueClassJsonValueSerializer<T>? = outerClazz.declaredMethods.find { method ->
            // JsonValueを持つstaticメソッドのみ対象とする
            // インスタンスメソッドに付与されるパターンはunboxされるのと等価なため無視してよい
            method.annotations.any { it is JsonValue } && Modifier.isStatic(method.modifiers)
        }?.let { ValueClassJsonValueSerializer(innerClazz, it) }
    }
}
```

次に、先ほど作成していた`ValueClassSerializeAnnotationIntrospector`に、このシリアライザに関する処理を追加します。
これによって、`JsonValue`の設定された`value class`は`ValueClassJsonValueSerializer`で、それ以外は`ValueClassBoxSerializer`で処理されるようになります。

```diff_kotlin
        // 取得したプロパティのKotlin上の戻り値がvalue classであれば、対応するシリアライザを返す
        returnClazz
            ?.takeIf { it.isValue }
            ?.java
            ?.let { outerClazz ->
+               val innerClazz = getter.returnType
+
+               ValueClassJsonValueSerializer.createdOrNull(outerClazz, innerClazz)
+                   ?: @Suppress("UNCHECKED_CAST")
+                   ValueClassBoxSerializer(outerClazz, getter.returnType)
-               @Suppress("UNCHECKED_CAST")
-               ValueClassBoxSerializer(outerClazz, getter.returnType)
            }
```

この修正を行うと、実行結果が以下のように変化します。
`JsonValue`に設定した値でシリアライズできるようになっています。

```diff_json:実行結果
{
  "foo" : 18446744073709551615,
  "bar" : "CustomSerializer 1",
  "baz" : "JsonSerializer 2",
+ "qux" : "JsonValue 3",
- "qux" : 3,
  "quux" : 4
} 
```

# 終わりに
この記事では`jackon-module-kotlin`で`value class`をシリアライズする方法についてまとめました。

`value class`は`Kotlin 1.5`で正式化した機能ですが、`Java`/`Kotlin`間の見え方の違いや、`Kotlin`のバージョンアップに伴い名称・記法の変更が有ったことから、`jackon-module-kotlin`での対応が難しい状況があります。
この記事が`jackson-module-kotlin`での対応を待たずに`value class`を使いたい方のお役に立てれば幸いです。

技術的な解説に関する記事も後日公開~~予定です~~しましたので、もしよろしければそちらもご覧下さい。

https://qiita.com/wrongwrong/items/ed3ba2aa108633abf452