jackon-module-kotlinでvalue class(inline class)をシリアライズする【解説編】

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

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

# 前書き
前回の記事では、`jackon-module-kotlin`で`value class`(`inline class`)をシリアライズする方法についてまとめました。

https://qiita.com/wrongwrong/items/28ba94fc3bb64d98c698

前回の記事ではどのようにすれば動くかに絞って解説したため、「なんでこの書き方をするの？/この書き方で動くの？」という疑問を抱いた方もいらっしゃったと思います（~~本当に？~~）。
そこで、この記事では前回の記事で紹介した内容の内、コードを読んでも分からないような点に関して、技術的な解説を行っていきます。

なお、記事では引き続き`jackson-module-kotlin 2.13.0`/`Kotlin 1.5.0`を前提にしています。

# シリアライズが異常になる原因
コードの解説に入る前に、前回紹介したシリアライズ関連の不具合が発生する原因について解説します。

この原因は、`Kotlin`上で`value class`が戻り値となる`getter`が、`Java`上で特殊な形になることです。
前回利用した`Target`クラスを例に解説します。

```kotlin:前回利用したシリアライズ対象クラス
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)
)
```

このクラスをデコンパイルし、`getter`のみ抜き出して整形すると以下のようになります。

```java:デコンパイル結果（getterのみ抜き出して整形済み）
public final class Target {
   public final long getFoo-s-VKNKU() { return this.foo; }
   public final int getBar-Eu_8zPo() { return this.bar; }
   @JsonSerialize(using = V2.CustomSerializer.class)
   public final int getBaz-18JmITk() { return this.baz; }
   public final int getQux-ePNLYys() { return this.qux; }
   public final int getQuux-6jTAmKA() { return this.quux;}
}
```

通常のクラスに比べると、以下2つの変化が起きています。

1. 戻り値が`unboxed`された値になっている
2. `getter`の名前にサフィックスが付いている

この内、今回の問題の原因になっているのは1です[^name]。
この変化が起きているため、シリアライズ時に参照される型が異なってしまい、期待した通りのシリアライズ結果が得られないという問題が発生します。

[^name]: 2の問題に関しては、`jackson-module-kotlin`の方で一応の対策が入っています。

## 補足: nullableな内容が絡む場合
前回の例では出すことができなかったため、`value class`の中身が`nullable`な場合と、プロパティが`nullable`な場合に関しても見てみます。

```kotlin
@JvmInline
value class Value(val value: Int?)

data class Data(
    val hoge: Value,
    val fuga: Value?
)
```

デコンパイル結果は以下のようになります。

```java:デコンパイル結果（getterのみ抜き出して整形済み）
public final class Data {
   @NotNull
   public final Integer getHoge-aj8DijA() { return this.hoge; }
   @Nullable
   public final Value getFuga-DESORPU() { return this.fuga; }
}
```

中身が`nullable`の場合`unbox`された形で取り扱われ、プロパティが`nullable`の場合`box`された形で取り扱われていることが分かります。

# ValueClassSerializeAnnotationIntrospectorについて
次に、`ValueClassSerializeAnnotationIntrospector`の実装について解説します。

<details><summary>`ValueClassSerializeAnnotationIntrospector`（全文）</summary><div>

```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 ->
                val innerClazz = getter.returnType

                ValueClassJsonValueSerializer.createdOrNull(outerClazz, innerClazz)
                    ?: @Suppress("UNCHECKED_CAST")
                    ValueClassBoxSerializer(outerClazz, innerClazz)
            }
    } 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 }
```
</div></details>

## findSerializerForValueClass関数について
`findSerializerForValueClass`関数は`value class`に対するシリアライザを返す関数です。

この関数で最も重要なのは、`value class`に対するシリアライザが必要かの判定です。
`getter`の戻り値が`Kotlin`上で`value class`でないか`box`されている場合、`Jackson`側にシリアライズを任せる必要があるため、この判定を行なっています。

```kotlin:value&nbsp;classに対するシリアライザが必要かの判定部分（抜粋）
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 }
```

## findNullSerializerもoverrideする理由について
`ValueClassSerializeAnnotationIntrospector`は`findNullSerializer`も`override`しています。
これは、`value class`の中身が`null`だった場合に`findSerializer`が呼び出されないためです。

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

# value classに対するカスタムシリアライザについて
次に、`value class`に対するカスタムシリアライザの実装について解説します。

<details><summary>`ValueClassBoxSerializer`（全文）</summary><div>

```kotlin:ValueClassBoxSerializer
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する
        val boxed: Any = boxMethod.invoke(null, value)
        // boxされた値に対するシリアライザを検索し、シリアライズする
        provider.findValueSerializer(outerClazz).serialize(boxed, gen, provider)
    }
}
```
</div></details>

<details><summary>`ValueClassJsonValueSerializer`（全文）</summary><div>

```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) }
    }
}
```
</div></details>

## value classをインスタンス化する方法について
まず、`value class`をインスタンス化する方法について解説します。

一般的な例として、以下のような`init`ブロックで値のチェックを行う`value class`について見ます。

```kotlin:value&nbsp;classの例
@JvmInline
value class Value(val value: Int) {
    init { if (value == 0) throw IllegalArgumentException("") }
}
```

このクラスのデコンパイル結果（重要な部分だけ抜粋）は以下のようになります。

```java:デコンパイル結果（整形・抜粋）
@JvmInline
@Metadata(/* 略 */)
public final class Value {
   private final int value;

   public final int getValue() { return this.value; }

   // $FF: synthetic method
   private Value(int value) { this.value = value; }

   public static int constructor-impl(int value) {
      if (value == 0) {
         throw (Throwable)(new IllegalArgumentException(""));
      } else {
         return value;
      }
   }

   // $FF: synthetic method
   public static final Value box-impl(int v) { return new Value(v); }

   // $FF: synthetic method
   public final int unbox-impl() { return this.value; }
}
```

このデコンパイル結果から以下の3点が分かります。

1. インスタンス生成には`box-impl`を用いる
2. `init`ブロック内の処理は`constructor-impl`に実装される
3. `box-impl`でもコンストラクタの実体でも`constructor-impl`は呼び出されない

つまり、一般に`value class`を適切にインスタンス化するには`constructor-impl` -> `box-impl`の順番で呼び出す必要があります。

一方、`ValueClassBoxSerializer`では`constructor-impl`の呼び出しを行なっていません。
これは、`serialize`関数の引数は`unbox`された値になることから、プログラム中のどこかでは`constructor-impl`が呼び出されていると考えられるためです[^use-reflect]。

[^use-reflect]: 言うまでもありませんが、リフレクションで不適切な取り扱いをされていた場合まで考慮すると、`constructor-impl`がどこでも呼び出されていない可能性はあります。

## value classのインスタンス関数の実体について
次に、`value class`のインスタンス関数の実体がどのようになるかについて解説します。
この解説には、前回の記事で利用した`V3`クラスを利用します。

```kotlin:前回の記事で利用したV3
@JvmInline
value class V3(val value: Int) {
    @get:JsonValue
    val jsonValue get() = "JsonValue $value"
}
```

このクラスのデコンパイル結果（`jsonValue`に関わる部分だけ抜粋）は以下のようになります。

```java:デコンパイル結果（整形・抜粋）
public final class V3 {
   @JsonValue
   @NotNull
   public static final String getJsonValue-impl(int $this) { return "JsonValue " + $this; }
}
```

デコンパイル結果の通り、`jsonValue`は`unbox`された値を引数に取る`static`メソッドになります。
通常のクラスと違い、インスタンスメソッドは生成されないため、`JsonValue`アノテーションが働かない状況となっています。

`ValueClassJsonValueSerializer`では、この`static`メソッドを呼び出すことでシリアライズを行なっています。

## serialize関数の入力について
通常のユースケースでは、値が`null`だった場合それに対応したシリアライザが呼び出されるため、`serialize`関数の引数`value`はほぼ確実に`non-null`となります。
一方、今回定義した2つのカスタムシリアライザでは引数を`nullable`にしています。

これは`findNullSerializer`も`override`している理由と同様に、`value class`の中身が`null`のパターンに対応するためです。

# JsonSerializeに設定したシリアライザの挙動について
最後に、`JsonSerialize`に設定したシリアライザの挙動についてです。

`JsonSerialize`が機能しない理由は、冒頭で紹介した通り、`Kotlin`上で`value class`が戻り値となる`getter`が、`Java`上で特殊な形になることです。
これに関しては、記事中で紹介した通り、`Serializer`の処理を`unbox`された値に対するものに書き換えることで対応することができます。

```kotlin:前回の記事で利用したV2
@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}")
        }
    }
}
```