jackson-module-kotlin の 2.10.0 と 2.10.1 でデフォルトの挙動が異なる

jackson-module-kotlinの2.10.0と2.10.1でデフォルトの挙動が異なる件を調べたメモです。

jackson-module-kotlinでisprefixが付くbooleanのpropertyをシリアライズした場合の挙動が
2.10.1から変わったので注意が必要でした。

下記バージョンで試してみます。

  • java 11.0.7
  • kotlin 1.4.20
  • jackson-databind 2.10.0
  • jackson-module-kotlin 2.10.0
  • jackson-databind 2.10.1
  • jackson-module-kotlin 2.10.1

Dependency

gradleを使って試してみます。
spring bootで簡単なcontrollerを作成して試してみます。
javalombok@Dataを指定した場合も比較したいのでlombokも追加してます。

build.gradle.kts

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compileOnly("org.projectlombok:lombok")
    annotationProcessor("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

mavenBomでjackson-bomのversionを明示的に指定します。
2.10.0と2.10.1を切り替えて試してみます。

dependencyManagement {
    imports {
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) {
//            extra["jackson-bom.version"] = "2.10.0"
            extra["jackson-bom.version"] = "2.10.1"
        }
    }
}

java

javaでPersonクラスを定義してみます。
booleanisTestUsertestUserを定義してみます。
それぞれlombokでも定義してみます。

isTestUserフィールド

    public static class JavaPerson {
        private final String name;
        private final boolean canWalk;
        private final boolean isTestUser;

        public JavaPerson(
                String name,
                boolean canWalk,
                boolean isTestUser
        ) {
            this.name = name;
            this.canWalk = canWalk;
            this.isTestUser = isTestUser;
        }

        public String getName() {
            return name;
        }

        public boolean isCanWalk() {
            return canWalk;
        }

        public boolean isTestUser() {
            return isTestUser;
        }
    }

testUserフィールド

    public static class JavaPersonNonIsPrefix {
        private final String name;
        private final boolean canWalk;
        private final boolean testUser;

        public JavaPersonNonIsPrefix(
                String name,
                boolean canWalk,
                boolean testUser
        ) {
            this.name = name;
            this.canWalk = canWalk;
            this.testUser = testUser;
        }

        public String getName() {
            return name;
        }

        public boolean isCanWalk() {
            return canWalk;
        }

        public boolean isTestUser() {
            return testUser;
        }
    }

@DataisTestUserフィールド

    @AllArgsConstructor
    @Data
    public static class JavaPersonUsingData {
        private String name;
        private boolean canWalk;
        private boolean isTestUser;
    }

@DatatestUserフィールド

    @AllArgsConstructor
    @Data
    public static class JavaPersonNonIsPrefixUsingData {
        private String name;
        private boolean canWalk;
        private boolean testUser;
    }

2.10.0

はじめに2.10.0で試してみます。

versionが2.10.0になってる事を確認。

$ gradle dependencies

|    |    +--- com.fasterxml.jackson.core:jackson-databind:2.10.5 -> 2.10.0
|    |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.10.0
|    |    |    \--- com.fasterxml.jackson.core:jackson-core:2.10.0
|    |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.5 -> 2.10.0
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.10.0
|    |    |    \--- com.fasterxml.jackson.core:jackson-databind:2.10.0 (*)
|    |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.5 -> 2.10.0
|    |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.10.0
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.10.0
|    |    |    \--- com.fasterxml.jackson.core:jackson-databind:2.10.0 (*)
|    |    \--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.5 -> 2.10.0
|    |         +--- com.fasterxml.jackson.core:jackson-core:2.10.0
|    |         \--- com.fasterxml.jackson.core:jackson-databind:2.10.0 (*)

+--- com.fasterxml.jackson.module:jackson-module-kotlin -> 2.10.0
|    +--- com.fasterxml.jackson.core:jackson-databind:2.10.0 (*)
|    +--- com.fasterxml.jackson.core:jackson-annotations:2.10.0

spring bootを起動して、それぞれを返すend pointにアクセスしてみます。
すべてtestUserとして返ってきています。

curl localhost:8080/java/vanilla | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/java/vanillaNonIsPrefix | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/java/usingData | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/java/nonIsPrefixUsingData | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}

2.10.1

今度は2.10.1で試してみます。

versionが2.10.1になってる事を確認。

gradle dependencies

|    |    +--- com.fasterxml.jackson.core:jackson-databind:2.10.5 -> 2.10.1
|    |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.10.1
|    |    |    \--- com.fasterxml.jackson.core:jackson-core:2.10.1
|    |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.5 -> 2.10.1
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.10.1
|    |    |    \--- com.fasterxml.jackson.core:jackson-databind:2.10.1 (*)
|    |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.5 -> 2.10.1
|    |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.10.1
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.10.1
|    |    |    \--- com.fasterxml.jackson.core:jackson-databind:2.10.1 (*)
|    |    \--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.5 -> 2.10.1
|    |         +--- com.fasterxml.jackson.core:jackson-core:2.10.1
|    |         \--- com.fasterxml.jackson.core:jackson-databind:2.10.1 (*)

+--- com.fasterxml.jackson.module:jackson-module-kotlin -> 2.10.1
|    +--- com.fasterxml.jackson.core:jackson-databind:2.10.1 (*)
|    +--- com.fasterxml.jackson.core:jackson-annotations:2.10.1

同様のクラスで試してみます。 すべてtestUserとして返ってきています。

curl localhost:8080/java/vanilla | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/java/vanillaNonIsPrefix | jq
{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/java/usingData | jq
{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/java/nonIsPrefixUsingData | jq
{
  "name": "test",
  "canWalk": true,
  "testUser": true
}

kotlin

kotlinでPersonクラスを定義してみます。
isTestUserというBooleanのpropertyを定義します。

普通のクラス

    class KotlinPerson(
        val name: String,
        val canWalk: Boolean,
        val isTestUser: Boolean
    )

data class

    data class KotlinPersonDataClass(
        val name: String,
        val canWalk: Boolean,
        val isTestUser: Boolean
    )

2.10.0

はじめに2.10.0で試してみます。
spring bootを起動して、それぞれを返すend pointにアクセスしてみます。
それぞれisTestUser propertyがtestUserとして返ってきています。
これはjavaでのデシリアライズと同様の挙動です。

curl localhost:8080/kotlin/class | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}
curl localhost:8080/kotlin/dataclass | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}

2.10.1

今度は2.10.1で試してみます。
すると、2.10.0ではtestUserで返ってましたが、2.10.1ではisTestUserとして返ってるのが分かります。

curl localhost:8080/kotlin/class | jq

{
  "name": "test",
  "canWalk": true,
  "isTestUser": true
}
curl localhost:8080/kotlin/dataclass | jq

{
  "name": "test",
  "canWalk": true,
  "isTestUser": true
}

これはjackson-module-kotlin 2.10.1で入った変更です。
https://github.com/FasterXML/jackson-module-kotlin/pull/256
https://github.com/FasterXML/jackson-module-kotlin/issues/80

この変更はjavaのクラスをkotlinに移行する時に困る場合があります。
(テストを入れてば検知出来ますが)
javaisprefixのbooleanのフィールドをそのままkotlinのpropertyとして移行すると、
突然デシリアライズの値がisXxxxで返ってくることになります。

@JsonProperty

2.10.1以降でproperty名を変更せずにデシリアライズのプロパティを変更するには
@JsonPropertyで名前を明示的に指定してあげればよいです。

    class KotlinPersonWithJsonProperty(
        val name: String,
        val canWalk: Boolean,
        @get:JsonProperty("testUser")
        val isTestUser: Boolean
    )

試してみると、testUserとして返ってきてます。

curl localhost:8080/kotlin/dataclassWithJsonProperty | jq

{
  "name": "test",
  "canWalk": true,
  "testUser": true
}

 
サンプルコードは下記にあげました。

おわり。