Skip to content

Issue with deserialization when there are unexpected properties (due to null StreamReadConstraints) #3913

Closed
@sbertault

Description

@sbertault

Hello,
We encountered a weird behaviour with jackson-core 2.15.0 that was not present in version 2.14.2 :
Disclaimer : this is a corner case (but still annoying ;))

When trying to unmarshall the raw json {"list":[{"type":"impl","unmappedKey":"unusedValue"}]}
with "type" being the discriminator for a sealed class, I get this error
com.fasterxml.jackson.databind.JsonMappingException: Cannot invoke "com.fasterxml.jackson.core.JsonParser.streamReadConstraints()" because "p" is null (through reference chain: com.orange.ccmd.connector.MyResponse["list"]->java.util.ArrayList[0])

In the debugger, there is no trace of a variable p being null though.
This happens only when there is all three conditions:

  • an extra field in the json that is not mapped to a field of the target class
  • an extra member in the class that is not present in the json.
  • a class hierarchy

Here is a complete working unit test showing the bug, with the stacktrace :

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test


internal class JacksonBugTest {

    private val objectMapper = ObjectMapper().apply {
        disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        registerModules(KotlinModule.Builder().build())
    }

    @Test
    fun thisFails() {
        val rawResponse = """{"list":[{"type":"impl","unmappedKey":"unusedValue"}]}"""
        shouldThrow<JsonMappingException> {
            objectMapper.readValue(rawResponse, MyResponse::class.java)
        }
        /**
         * com.fasterxml.jackson.databind.JsonMappingException: Cannot invoke "com.fasterxml.jackson.core.JsonParser.streamReadConstraints()" because "p" is null (through reference chain: com.orange.ccmd.connector.MyResponse["list"]->java.util.ArrayList[0])
         * 	at app//com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:402)
         * 	at app//com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:373)
         * 	at app//com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:375)
         * 	at app//com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
         * 	at app//com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
         * 	at app//com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545)
         * 	at app//com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:568)
         * 	at app//com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:439)
         * 	at app//com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1409)
         * 	at app//com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352)
         * 	at app//com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
         * 	at app//com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
         * 	at app//com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4825)
         * 	at app//com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3772)
         * 	at app//com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3740)
         * 	at app//com.orange.ccmd.connector.utils.CustomJacksonObjectMapperFactory.readObject(CustomJacksonJsonProvider.kt:36)
         */
    }

    @Test
    fun `is ok if no missing field in json`() {
        val rawResponse = """{"list":[{"type":"impl","missingInJson":"not missing","unmappedKey":"unusedValue"}]}"""
        val marshalled = objectMapper.readValue(rawResponse, MyResponse::class.java)
        marshalled shouldBe MyResponse(
            list = listOf(Base.Impl(
                type = "impl",
                missingInJson = "not missing"
            ))
        )
    }

    @Test
    fun `is ok if no extra field in json`() {
        val rawResponse = """{"list":[{"type":"impl"}]}"""
        val marshalled = objectMapper.readValue(rawResponse, MyResponse::class.java)
        marshalled shouldBe MyResponse(
            list = listOf(Base.Impl(
                type = "impl",
                missingInJson = null
            ))
        )
    }

    @Test
    fun `is ok if no class hierarchy`() {
        val rawResponse = """{"list":[{"type":"impl"}]}"""
        val marshalled = objectMapper.readValue(rawResponse, MyResponseWithoutHierarchy::class.java)
        marshalled shouldBe MyResponseWithoutHierarchy(
            list = listOf(Base.Impl(
                type = "impl",
                missingInJson = null
            ))
        )
    }
}

data class MyResponse(
    val list: List<Base>
)

data class MyResponseWithoutHierarchy(
    val list: List<Base.Impl>
)

sealed class Base  {
    companion object {
        @JsonCreator
        @JvmStatic
        fun unmarshall(
            @JsonProperty("missingInJson") missingInJson: String? = null,
            @JsonProperty("type") type: String? = null,
        ): Base? {
            return when (type) {
                "impl" -> Impl(
                    missingInJson = missingInJson,
                    type = type,
                )
                else -> null
            }
        }
    }

    data class Impl(
        val type: String? = null,
        val missingInJson: String? = null
    ) : Base()
}

Thanks for the invaluable work

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions