🧩

Jacksonで単一プロパティを持つdata classをJSONのプリミティブ型として表現する

2020/04/12に公開

Value Objectとして使っている、単一プロパティを持つdata classを、JacksonでシンプルなJSONとして表現するのにちょっと手こずったのでまとめておきます。

問題設定

例として、次のようなクラス定義を考えます。

data class User(val userId: UserId, val userName: UserName)
data class UserId(val value: Int)
data class UserName(val value: String)

UserオブジェクトをJacksonでシリアライズした場合、普通だと次のようになります。

{"userId": {"value": 1}, "userName": {"value": "sato"}}

これを次のようにシリアライズした上で、元の形にデシリアライズしたいというのが、この記事の主題です。

{"userId": 1, "userName": "sato"}

なお、 UserIdUserNameUser 以外のクラスからも使われる可能性があります。これらを使うクラス間で挙動を統一できるよう、クラス定義に手を加えるなら User よりも UserIdUserName に加えたいです。

結論

次のように、 UserIdUserName にアノテーション @JsonCreator@JsonValue をつけると実現できます。

data class User(val userId: UserId, val userName: UserName)
data class UserId @JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(@JsonValue val value: Int)
data class UserName @JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(@JsonValue val value: String)

解説

まず、 @JsonValue はシリアライズに影響を与えるアノテーションで、シリアライズ結果を取得するメソッドに付けます。

次に、 @JsonCreator はデシリアライズに影響を与えるアノテーションで、JSONの値からオブジェクトを作るためのコンストラクターやファクトリーメソッドに付けます。Javaの場合はコンストラクターに @JsonCreator をつけるだけでいいのですが、Kotlin Moduleを使っている場合はうまく行かないので、 mode = JsonCreator.Mode.DELEGATING が必要です [1]

なお、Kotlinでプライマリコンストラクターにアノテーションをつけるには、 constructor キーワードが必要です [2]

コード

実際のコードは次のようになります。

build.gradleの一部

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    implementation "com.fasterxml.jackson.core:jackson-databind:2.9.10.4"
    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.10"
}

Main.kt

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

fun main() {
    val u = User(UserId(1), UserName("sato"))
    println(u)

    val mapper = jacksonObjectMapper()
    val json = mapper.writeValueAsString(u)
    println(json)

    val parsed: User = mapper.readValue(json)
    println(parsed)
}

data class User(val userId: UserId, val userName: UserName)
data class UserId @JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(@JsonValue val value: Int)
data class UserName @JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(@JsonValue val value: String)

実行結果

User(userId=UserId(value=1), userName=UserName(value=sato))
{"userId":1,"userName":"sato"}
User(userId=UserId(value=1), userName=UserName(value=sato))

参考

脚注
  1. @jsonCreator on primary constructor is not working · Issue #305 · FasterXML/jackson-module-kotlin
    https://github.com/FasterXML/jackson-module-kotlin/issues/305 ↩︎

  2. https://kotlinlang.org/docs/reference/annotations.html#usage ↩︎

Discussion