【Jackson】Converterを使い、シリアライザーを別の型へ適用する

# やること
`Java UUID`のためのシリアライザーを`Kotlin UUID`で再利用する状況を例に、ある型のためのシリアライザーを別の型で機能させる方法を紹介します。

特に`Kotlin`では、（多分）マルチプラットフォーム向けに再実装されている`Java`の基本的なクラスがあるため、それらに対して`Java`向けの資産を使い回すのに役立ちます。
あるいは、`Java`の基本的なクラスをラップするような値オブジェクトの処理にも役立つかもしれません。

# やり方
以下は今回紹介するコード全体のまとめです。

<details>

```kotlin
@file:OptIn(ExperimentalUuidApi::class, ExperimentalEncodingApi::class)
package org.wrongwrong

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.databind.util.StdConverter
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.nio.ByteBuffer
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.toJavaUuid
import kotlin.uuid.toKotlinUuid
import java.util.UUID as JavaUuid
import kotlin.uuid.Uuid as KotlinUuid

class JavaUuidToBase64Serializer : StdSerializer<JavaUuid>(JavaUuid::class.java) {
    override fun serialize(
        value: JavaUuid,
        gen: JsonGenerator,
        provider: SerializerProvider
    ) {
        val buffer: ByteBuffer = ByteBuffer.allocate(16).apply {
            putLong(value.mostSignificantBits)
            putLong(value.leastSignificantBits)
        }

        gen.writeString(Base64.encode(buffer.array()))
    }
}

object KotlinUuidToJavaConverter : StdConverter<KotlinUuid, JavaUuid>() {
    override fun convert(input: KotlinUuid): JavaUuid = input.toJavaUuid()
}

class AI : NopAnnotationIntrospector() {
    override fun findSerializationConverter(a: Annotated): Any? = (a as? AnnotatedMethod)
        ?.takeIf { it.rawReturnType == KotlinUuid::class.java }
        ?.let { _ -> KotlinUuidToJavaConverter }
}

data class Dto(
    @JsonSerialize(using = JavaUuidToBase64Serializer::class)
    val java: JavaUuid,
    @JsonSerialize(using = JavaUuidToBase64Serializer::class)
    val kotlin: KotlinUuid,
)

fun main() {
    val uuid = JavaUuid.randomUUID()
    val dto = Dto(uuid, uuid.toKotlinUuid())

    val mapper = jacksonObjectMapper().registerModule(
        object : SimpleModule() {
            override fun setupModule(context: SetupContext) {
                context.appendAnnotationIntrospector(AI())
            }
        }
    )

    println(mapper.writeValueAsString(dto))
}
```
</details>

## 前提
シリアライザーは以下を使います。

```kotlin:Java UUIDをBase64エンコードするシリアライザー
@file:OptIn(ExperimentalEncodingApi::class)
package org.wrongwrong

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.nio.ByteBuffer
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import java.util.UUID as JavaUuid

class JavaUuidToBase64Serializer : StdSerializer<JavaUuid>(JavaUuid::class.java) {
    override fun serialize(
        value: JavaUuid,
        gen: JsonGenerator,
        provider: SerializerProvider
    ) {
        val buffer: ByteBuffer = ByteBuffer.allocate(16).apply {
            putLong(value.mostSignificantBits)
            putLong(value.leastSignificantBits)
        }

        gen.writeString(Base64.encode(buffer.array()))
    }
}
```

何も対策せずに上記のシリアライザーで`Kotlin UUID`を処理すると、当然型不一致（`java.lang.ClassCastException: kotlin.uuid.Uuid cannot be cast to java.util.UUID`）でエラーになります。

## やり方1: `JsonSerialize(converter = ...)`を指定する
`Jackson`には、`Converter`という機能が有ります。
これを使うことで、変換結果に対しシリアライザーを適用できます。

https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/util/Converter.html

`Kotlin UUID`から`Java UUID`への`Converter`実装は以下のようになります[^conv]。

[^conv]: `Javadoc`では`StdConverter`を使わないことが推奨されていたため、サンプルも`Converter`インターフェースを直で実装しています。

```kotlin
@file:OptIn(ExperimentalUuidApi::class)
package org.wrongwrong

import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.Converter
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.toJavaUuid
import java.util.UUID as JavaUuid
import kotlin.uuid.Uuid as KotlinUuid

class KotlinUuidToJavaConverter : Converter<KotlinUuid, JavaUuid> {
    // ※inputがnullの際は呼ばれない
    override fun convert(input: KotlinUuid): JavaUuid = input.toJavaUuid()

    override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(KotlinUuid::class.java)
    override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(JavaUuid::class.java)
}
```

これを`JsonSerialize(converter = ...)`に指定することで、前述のシリアライザーが機能するようになります。

```kotlin
@file:OptIn(ExperimentalUuidApi::class)
package org.wrongwrong

import com.fasterxml.jackson.databind.annotation.JsonSerialize
import kotlin.uuid.ExperimentalUuidApi
import java.util.UUID as JavaUuid
import kotlin.uuid.Uuid as KotlinUuid

data class Dto(
    @JsonSerialize(using = JavaUuidToBase64Serializer::class)
    val java: JavaUuid,
    // converterによってJava UUIDとして処理できるようになる
    @JsonSerialize(using = JavaUuidToBase64Serializer::class, converter = KotlinUuidToJavaConverter::class)
    val kotlin: KotlinUuid,
)
```

## やり方2: `AnnotationIntrospector::findSerializationConverter`を実装する
一々アノテーションで指定するのが面倒な場合、`AnnotationIntrospector::findSerializationConverter`を実装する方法が有ります。

```kotlin
@file:OptIn(ExperimentalUuidApi::class)
package org.wrongwrong

import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.Converter
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.toJavaUuid
import java.util.UUID as JavaUuid
import kotlin.uuid.Uuid as KotlinUuid

class KotlinUuidToJavaConverter : Converter<KotlinUuid, JavaUuid> {
    // ※inputがnullの際は呼ばれない
    override fun convert(input: KotlinUuid): JavaUuid = input.toJavaUuid()

    override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(KotlinUuid::class.java)
    override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(JavaUuid::class.java)
}

class AI : NopAnnotationIntrospector() {
    override fun findSerializationConverter(a: Annotated): Any? = (a as? AnnotatedMethod)
        ?.takeIf { it.rawReturnType == KotlinUuid::class.java }
        ?.let { _ -> KotlinUuidToJavaConverter() }
}
```

これは`ObjectMapper`へ指定することで利用できます。

```kotlin
package org.wrongwrong

import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper

val mapper = jacksonObjectMapper().registerModule(
    object : SimpleModule() {
        override fun setupModule(context: SetupContext) {
            context.appendAnnotationIntrospector(AI())
        }
    }
)
```

ただし、このやり方では`Kotlin UUID`が全て`Java UUID`扱いになる = `Kotlin UUID`に対するシリアライザーは機能しなくなる点に注意が必要です。

# 余談
`jackson-module-kotlin`における`value class`のシリアライズ処理に関しても、裏ではこれを利用しています。