Search code examples
jsonkotlinjacksonjackson-databind

Field name "Parts" in JSON causes deserialization to fail


In the following code I can trigger failure of the deserialization due to

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList<de.richtercloud.jackson.deserialize.list.of.objects.DeserializationTest$D>` out of FIELD_NAME token
 at [Source: (String)"{
            "a": [
                {
                    "c": {
                        "Parts": []
                    }
                }
            ]
        }"; line: 5, column: 25] (through reference chain: de.richtercloud.jackson.deserialize.list.of.objects.DeserializationTest$A["a"]->java.util.ArrayList[0]->de.richtercloud.jackson.deserialize.list.of.objects.DeserializationTest$B["c"])

    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
    at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1442)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1216)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1168)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.handleNonArray(CollectionDeserializer.java:332)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:265)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1284)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:530)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:528)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:417)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:530)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:528)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:417)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173)
    at de.richtercloud.jackson.deserialize.list.of.objects.DeserializationTest.testDeserialization(DeserializationTest.kt:22)
[omitted JUnit stacktrace]

by changing the name of the Parts field (both in JSON and class at the same time) in the following test:

internal class DeserializationTest {

    @Test
    fun testDeserialization() {
        val serialized = """{
            "a": [
                {
                    "c": {
                        "Parts": []
                    }
                }
            ]
        }"""
        val deserialized = createObjectMapper().readValue(serialized, A::class.java)
        assertNotNull(deserialized)
    }

    fun createObjectMapper() = ObjectMapper().registerModule(KotlinModule())

    data class A(val a: List<B>)

    data class B(val c: C)

    data class C(val Parts: List<D>)

    data class D(var e: String)
}

I expect this test to work no matter which name I choose because the field name is data and shouldn't have any impact on the behaviour of the deserialization algorithm.

I tried parts, a and many other names and searched online and in the jackson-databind source code for hints that "Parts" is a keyword.

Adding .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) to the ObjectMapper initialization causes the exception message to change to com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class de.richtercloud.jackson.deserialize.list.of.objects.DeserializationTest$D] value failed for JSON property e due to missing (therefore NULL) value for creator parameter e which is a non-nullable type.

I'm using the following versions:

<properties>
    <kotlin.version>1.3.61</kotlin.version>
    <junit5.version>5.5.2</junit5.version>
    <jackson.version>2.10.1</jackson.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib</artifactId>
        <version>${kotlin.version}</version>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
        <version>${kotlin.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-kotlin</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <!-- test dependencies -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>${junit5.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

I can work around the exception by specifying

data class C(@JsonProperty("Parts") val parts: List<D>)

however I'd like to understand this problem/behaviour.


Solution

  • This is a "feature" of Jackson: https://github.com/FasterXML/jackson-module-kotlin/issues/92

    Suggested solution is to use @JsonProperty as you already do.

    Having only snake_case keys should also work fine, though:

    internal class DeserializationTest {
    
        @Test
        fun testDeserialization() {
            val serialized = """{
                "a": [
                    {
                        "c": {
                            "parts": []
                        }
                    }
                ]
            }"""
            val deserialized = createObjectMapper().readValue(serialized, A::class.java)
            assertNotNull(deserialized)
        }
    
        fun createObjectMapper() = ObjectMapper().registerModule(KotlinModule())
    
        data class A(val a: List<B>)
    
        data class B(val c: C)
    
        data class C(val parts: List<D>)
    
        data class D(var e: String)
    }