やること
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))
}
前提
シリアライザーは以下を使います。
@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
のシリアライズ処理に関しても、裏ではこれを利用しています。
-
Javadoc
ではStdConverter
を使わないことが推奨されていたため、サンプルもConverter
インターフェースを直で実装しています。 ↩