Skip to content
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

Deserialization fails for class with single private accessor property #753

Open
1 task done
hotire opened this issue Jan 5, 2024 · 5 comments
Open
1 task done
Labels

Comments

@hotire
Copy link

hotire commented Jan 5, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Deserialization fails for class with single private accessor property.

When deserializing, a MismatchedInputException occurs.

To Reproduce

class A(
    private val name: String,
)
class B(
    private val name: String,
    private val age: Int,
)

private val mapper: ObjectMapper = jacksonObjectMapper()

@Test
fun fail() {
    // given
    val json = """
        {"name" : "hello"}
    """.trimIndent()
    // when, then
    shouldThrow<MismatchedInputException> {
        mapper.readValue(json, A::class.java)
    }
}

@Test
fun success() {
    // given
    val json = """
        {"name" : "hello", "age": 12}
    """.trimIndent()
    shouldNotThrowAny {
        mapper.readValue(json, B::class.java)
    }
}

Expected behavior

A class with single private accessor property is properly deserialized.

Actual behavior

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `test.SimpleTest$A` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"name" : "hello"}"; line: 1, column: 2]

	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)

Versions

Kotlin: 1.8.0
Jackson-module-kotlin: 2.12.7
Jackson-databind: 2.12.7.1

Additional context

I tried to dig into this and discovered the flow below:
(Code has been simplified)

  1. In the "BasicDeserializerFactory _addExplicitAnyCreator" method, it is processed according to the number of constructor parameters.
 protected void _addExplicitAnyCreator(DeserializationContext ctxt,
            BeanDescription beanDesc, CreatorCollector creators,
            CreatorCandidate candidate, ConstructorDetector ctorDetector)
        throws JsonMappingException
    {
        // Looks like there's bit of magic regarding 1-parameter creators; others simpler:
        if (1 != candidate.paramCount()) {

        }
    }
  1. Because of the private accessor, the useProps variable is set to false, so _creators[C_PROPS] of CreatorCollector cannot be set.
default:
            { // Note: behavior pre-Jackson-2.12
                final BeanPropertyDefinition paramDef = candidate.propertyDef(0);
                // with heuristic, need to start with just explicit name
                paramName = candidate.explicitParamName(0);

                // If there's injection or explicit name, should be properties-based
                useProps = (paramName != null) || (injectId != null);
                if (!useProps && (paramDef != null)) {
                    // One more thing: if implicit name matches property with a getter
                    // or field, we'll consider it property-based as well
        
                    // 25-May-2018, tatu: as per [databind#2051], looks like we have to get
                    //    not implicit name, but name with possible strategy-based-rename
        //            paramName = candidate.findImplicitParamName(0);
                    paramName = candidate.paramName(0);
                    useProps = (paramName != null) && paramDef.couldSerialize();
                }
            }

if (useProps) {
   SettableBeanProperty[] properties = new SettableBeanProperty[] {
                    constructCreatorProperty(ctxt, beanDesc, paramName, 0, param, injectId)
      };
   creators.addPropertyCreator(candidate.creator(), true, properties);
   return;
}
  1. The MismatchedInputException occurs because "_propertyBasedCreator" is null in the "BeanDeserializer.deserializeFromObjectUsingNonDefault" method
@hotire hotire added the bug label Jan 5, 2024
@k163377
Copy link
Contributor

k163377 commented Jan 7, 2024

Thank you for digging deeper.

By the way, is this a bug in KotlinModule?
If databind is the cause, please check there and report.

As a side note, if you are creating a Java only sample code regarding deserialization, you need to set JsonCreator in the constructor.

@hotire
Copy link
Author

hotire commented Jan 7, 2024

Hello.

In case of Java it succeeds.

The test code is below.

class JacksonJavaTest {

    final ObjectMapper mapper = new ObjectMapper();

    static class A {
        private final String name;

        @JsonCreator
        public A(@JsonProperty("name") String name){
            this.name = name;
        }
    }

    static class B {
        private final String name;
        private final int age;

        @JsonCreator
        public B(
                @JsonProperty("name")String name,
                @JsonProperty("age")int age){
            this.name = name;
            this.age = age;
        }
    }

    @Test
    void success() throws JsonProcessingException {
        final String json = "{\"name\" : \"hello\", \"age\": 12}";
        mapper.readValue(json, B.class);
    }

    @Test // expected fail but success
    void fail() throws JsonProcessingException {
        final String json = "{\"name\" : \"hello\"}";
        mapper.readValue(json, A.class);
    }
}

@marilatte53
Copy link

I'm having the same problem when deserializing the following class:

class StringFromListGenerator(
    val options: List<String> // Bug in Kotlin module: this cannot be private
) : ArgumentGenerator {
    override fun generate(random: Random): String {
        return options[random.nextInt(options.size)]
    }
}

When I try to deserialize this from JSON:

{
  "options": [
    "ABC",
    "DEF",
    "123"
  ]
}

I get a misleading error message:

Cannot deserialize value of type java.util.ArrayList<java.lang.String> from Object value (token JsonToken.FIELD_NAME)

@k163377
Copy link
Contributor

k163377 commented Feb 4, 2024

I have been very busy for a while and do not have time to check this issue.

Personally, I think the problem is related to AnnotationIntrospector::findImplicitPropertyName.
In kotlin-module, it is implemented as KotlinNamesAnnotationIntrospector::findImplicitPropertyName.

The sample code you gave us uses JsonProperty to name it, but in kotlin-module it is named using the above function.
In other words, the reproduced code in Java that you gave me does not actually reproduce the behavior of kotlin-module completely.

I have a vague recollection that a similar issue was previously posted on kotlin-module and was closed as a duplication of a databind or core issue.

Your help in a more in-depth investigation may speed up the resolution of the issue.

@jool
Copy link

jool commented Jun 10, 2024

Here is a possibly related example. I am not sure if the root cause is the same, perhaps I should open a new issue for this but this was similar enough to entice me to first post this as a comment here.

The interesting part here is the behaviour of deser into class B, which has a single field that unconventionally starts with an uppercase

data class A(
    val foo: String,
)

data class B(
    val Foo: String
)

data class C(
    val Foo: String,
    val Bar: String
)

data class D(
    @JsonProperty("Foo")
    val Foo: String,
)

fun main() {
    val om = ObjectMapper().apply {
        registerKotlinModule()
        disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
        disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    }
    val json = """{"foo": "bar", "Foo": "Foo", "Bar": "Bar", "a": "b"}  """

    om.readValue<A>(json) // Works
    om.readValue<B>(json) // Fails with Exception in thread "main" ... Cannot construct instance of B` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)

    om.readValue<C>(json) // Works
    om.readValue<D>(json) // Works
}

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

4 participants