Skip to content

@JsonCreator that worked in 2.17.3 no longer works in 2.19.0 #997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
2 of 4 tasks
Mahoney opened this issue May 15, 2025 · 5 comments
Open
2 of 4 tasks

@JsonCreator that worked in 2.17.3 no longer works in 2.19.0 #997

Mahoney opened this issue May 15, 2025 · 5 comments
Labels

Comments

@Mahoney
Copy link

Mahoney commented May 15, 2025

Search before asking

  • I searched in the issues and found nothing similar.
  • I have confirmed that the same problem is not reproduced if I exclude the KotlinModule.
  • I searched in the issues of databind and other modules used and found nothing similar.
  • I have confirmed that the problem does not reproduce in Java and only occurs when using Kotlin and KotlinModule.

Describe the bug

A null json property mapped to this class:

data class Limit(
  @field:JsonValue
  val limit: Long?,
) {
  companion object {

    @JvmStatic
    @get:JsonCreator(mode = DEFAULT)
    val UNLIMITED = Limit(null)

    @JvmStatic @JsonCreator
    fun fromNullable(limit: Long?): Limit = if (limit == null) UNLIMITED else Limit(limit)
  }
}

used to work in 2.17.3, returning the UNLIMITED instance, if the property on the parent class was marked @JsonSetter(nulls = AS_EMPTY).

In 2.19.0 it throws this exception:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot create empty instance of [simple type, class foo.Limit], no default Creator
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1915)
	at com.fasterxml.jackson.databind.deser.std.StdDeserializer._findNullProvider(StdDeserializer.java:2176)
	at com.fasterxml.jackson.databind.deser.std.StdDeserializer.findValueNullProvider(StdDeserializer.java:2094)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase._resolveMergeAndNullSettings(BeanDeserializerBase.java:1101)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:604)
	at com.fasterxml.jackson.module.blackbird.deser.SuperSonicBeanDeserializer.resolve(SuperSonicBeanDeserializer.java:79)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:348)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:285)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:175)
	at com.fasterxml.jackson.databind.DeserializationContext.findNonContextualValueDeserializer(DeserializationContext.java:659)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:555)
	at com.fasterxml.jackson.module.blackbird.deser.SuperSonicBeanDeserializer.resolve(SuperSonicBeanDeserializer.java:79)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:348)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:285)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:175)
	at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:669)
	at com.fasterxml.jackson.databind.ObjectReader._findRootDeserializer(ObjectReader.java:2411)
	at com.fasterxml.jackson.databind.ObjectReader._bind(ObjectReader.java:2101)
	at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1248)
	at com.fasterxml.jackson.jakarta.rs.base.ProviderBase.readFrom(ProviderBase.java:791)

To Reproduce

package foo

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DEFAULT
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.annotation.Nulls.AS_EMPTY
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import foo.Limit.Companion.UNLIMITED

data class Limit(
  @field:JsonValue
  val limit: Long?,
) {

  companion object {

    @JvmStatic
    @get:JsonCreator(mode = DEFAULT)
    val UNLIMITED = Limit(null)

    @JvmStatic @JsonCreator
    fun fromNullable(limit: Long?): Limit = if (limit == null) UNLIMITED else Limit(limit)
  }
}

data class Wrapper(
  @JsonSetter(nulls = AS_EMPTY) val limit: Limit = UNLIMITED,
)

fun main() {
  println(jacksonObjectMapper().readValue("{}", Wrapper::class.java))
}

Expected behavior

Prints Wrapper(limit=unlimited)

Versions

Kotlin: 2.1.21
Jackson-module-kotlin: 2.19.0
Jackson-databind: 2.19.0

Additional context

N/A

@Mahoney Mahoney added the bug label May 15, 2025
@Mahoney
Copy link
Author

Mahoney commented May 15, 2025

I couldn't work out what the Java equivalent would be to see if it were a databind change.

@k163377
Copy link
Contributor

k163377 commented May 17, 2025

I do not understand how you expect this DTO to be handled.
Can you please submit a more exhaustive case for the input JSON and the expected deserialization result?
Although not required, a test format using JUnit5 is preferred.

Perhaps due to the JsonCreator handling changes in 2.18, but it seems odd to have multiple JsonCreator for the same DTO.
I feel that the following changes should be made, for example

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
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class GitHub997 {
    data class Limit(
        @field:JsonValue
        val limit: Long?,
    ) {

        companion object {
            val UNLIMITED = Limit(null)

            @JvmStatic
            @JsonCreator
            fun fromNullable(limit: Long?): Limit = if (limit == null) UNLIMITED else Limit(limit)
        }
    }

    data class Wrapper(
        val limit: Limit = Limit.Companion.UNLIMITED,
    )

    @Test
    fun test() {
        assertEquals(Wrapper(), jacksonObjectMapper().readValue<Wrapper>("{}"))
    }
}

@Mahoney
Copy link
Author

Mahoney commented May 19, 2025

I think I've got a minimum viable reproducer...

For me this passes with com.fasterxml.jackson:jackson-bom:2.17.3 and fails with com.fasterxml.jackson:jackson-bom:2.18.0 and higher.

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.annotation.Nulls.AS_EMPTY
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import mocklab.GitHub997.Limit.Companion.UNLIMITED
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class GitHub997 {
  data class Limit(
    @field:JsonValue
    val limit: Long?,
  ) {

    companion object {
      val UNLIMITED = Limit(null)

      @JvmStatic
      @JvmOverloads
      @JsonCreator
      fun fromNullable(limit: Long? = null): Limit = if (limit == null) UNLIMITED else Limit(limit)
    }
  }

  data class Wrapper(
    @JsonSetter(nulls = AS_EMPTY)
    val limit: Limit = UNLIMITED,
  )

  val objectMapper = jacksonObjectMapper()

  @Test
  fun testWithNoLimit() {
    val deserialized = objectMapper.readValue<Wrapper>("""{  }""")
    assertEquals(UNLIMITED, deserialized.limit)
  }

  @Test
  fun testWithNullLimit() {
    val deserialized = objectMapper.readValue<Wrapper>("""{ "limit": null }""")
    assertEquals(UNLIMITED, deserialized.limit)
  }

  @Test
  fun testWithSpecificLimit() {

    val deserialized = objectMapper.readValue<Wrapper>("""{ "limit": 12 }""")
    assertEquals(Wrapper(limit = Limit(12)), deserialized)
  }
}

@k163377
Copy link
Contributor

k163377 commented May 24, 2025

Thank you for submitting a detailed test case.

It is true that a destructive change in behavior has occurred, but I personally feel that the problem is a deficiency in the definition of DTO.
For example, the following changes would succeed for both 2.18 and 2.19.

class GitHub997 {
    data class Limit(
        @JsonValue
        val limit: Long?,
    ) {
        companion object {
            val UNLIMITED = Limit(null)

            @JvmStatic
            @JsonCreator
            fun creator(limit: Long): Limit = Limit(limit)
        }
    }

    data class Wrapper(
        @JsonSetter(nulls = Nulls.SKIP)
        val limit: Limit = Limit.UNLIMITED,
    )

    val objectMapper = jacksonObjectMapper()

    @Test
    fun testWithNoLimit() {
        val deserialized = objectMapper.readValue<Wrapper>("""{  }""")
        assertEquals(Limit.UNLIMITED, deserialized.limit)
    }

    @Test
    fun testWithNullLimit() {
        val deserialized = objectMapper.readValue<Wrapper>("""{ "limit": null }""")
        assertEquals(Limit.UNLIMITED, deserialized.limit)
    }

    @Test
    fun testWithSpecificLimit() {
        val deserialized = objectMapper.readValue<Wrapper>("""{ "limit": 12 }""")
        assertEquals(Wrapper(limit = Limit(12)), deserialized)
    }
}

First, if you want to use default arguments when the value is null, you should use Nulls.SKIP.
Also, a JsonCreator with the DELEGATING mode specified will not be called when null is entered (the property is considered null).

@Mahoney
Copy link
Author

Mahoney commented May 27, 2025

Yup, that worked, thanks. Would you like me to close this as won't fix then?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants