1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

やること

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

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

やり方

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

@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))
}

前提

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

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という機能が有ります。
これを使うことで、変換結果に対しシリアライザーを適用できます。

Kotlin UUIDからJava UUIDへのConverter実装は以下のようになります1

@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 = ...)に指定することで、前述のシリアライザーが機能するようになります。

@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を実装する方法が有ります。

@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へ指定することで利用できます。

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のシリアライズ処理に関しても、裏ではこれを利用しています。

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?