FasterXML / jackson-modules-java8

Set of support modules for Java 8 datatypes (Optionals, date/time) and features (parameter names)
Apache License 2.0
398 stars 116 forks source link

Parameter names: Class with a single-parameter implicit constructor fails deserialization, works if more (unused) parameters are added #315

Closed kaqqao closed 2 months ago

kaqqao commented 2 months ago

Description

A class with a single-parameter implicit constructor will fail deserialization when the parameter-names module is used to discover the names. But if another (unused) parameter is added, deserialization will work fine.

Version

2.17.1

Reproduction

public static class Book {
    public Book(String title) {}
}

public static void main(String[] args) throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper().registerModule(new ParameterNamesModule());
    Book book = mapper.readValue("{\"title\":\"boom\");
}

This will throw:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of io.leangen.graphql.JsonTypeMappingTest$Book (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 1, column: 2] at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1754) at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1379) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1508) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3816)

But if another field is added (even if unused) to the Book's constructor:

public static class Book {
    public Book(String title, String dummy) {}
}

deserialization suddenly passes. Adding an explicit @JsonProperty("title") avoids the issue.

Expected behavior

Deserialization should either pass or fail in both situations. A dummy unused parameter should not have such an effect.

cowtowncoder commented 2 months ago

Unfortunately this is due to inherent ambiguity of 1-parameter constructor: it can either match 1-property JSON Object (name/value) OR be delegating (the whole JSON value matching value type of the one parameter. Same is not true for 2- or more parameter constructor.

In case of 1-parameter, implicit detection of Creators will try to determine type heuristically, basically:

  1. If @JsonValue found -> Delegating
  2. If there is a property implied by getter (like getTitle()) -> Properties
  3. Otherwise use ConstructorDetector setting if defined (not set by default
  4. If no determination, default to Delegating

In this case I think heuristics end up selecting Delegating.

2.18 does simplify handling slightly but basic logic remains the same.

This is why it is recommended that 1-parameter cases are annotated with

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)

(or adding @JsonProperty)

kaqqao commented 2 months ago

Ooh, I see. Thanks for explaining, much appreciated 🙏

cowtowncoder commented 2 months ago

@kaqqao No problem, this is not an intuitive thing unfortunately.