Deserialization fails with version after 2.17.3 when DTO doesn't have no-args constructor
Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
Looks like DTO without no-ars constructor is no longer able to to used for desirialization.
I'm upgrading my spring boot app and it seems jackson is also upgraded from 2.17.3 to 2.18+ My DTO/Bean looks like this:
public class Foo {
private final String id;
private String name;
!!!! NO DEFAULT CONSTRUCTOR !!!!!
public Foo(String id) {
this.id = id;
}
public Foo(String id, String name) {
this.id = id;
this.name = name;
}
------------------- getters/setters
}
Here it is the isolated code that demonstrates the issue. It works fine with 2.17.3 but doesn't with anything beyond that (2.18, 2.19, 2.20)
JsonMapper objectMapper = JsonMapper.builder()
.constructorDetector(ConstructorDetector.DEFAULT).build();
objectMapper.registerModule(new ParameterNamesModule());
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
Foo foo = objectMapper.readValue("{}", Foo.class);
It also fails to deserialize
String content = objectMapper.writeValueAsString(new Foo("blah", "qqq"));
Foo foo1 = objectMapper.readValue(content, Foo.class);
The exception is the following:
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `org.example.Foo` (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:1512)
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:4931)
```
If I add no-args constructor it works. But in my application modifying DTO/Bean is not always possibe.
Is this expected ? I can't find the docs explaining why it should not work.
### Version Information
2.18.3
### Reproduction
My DTO/Bean looks like this:
```java
public class Foo {
private final String id;
private String name;
!!!! NO DEFAULT CONSTRUCTOR !!!!!
public Foo(String id) {
this.id = id;
}
public Foo(String id, String name) {
this.id = id;
this.name = name;
}
------------------- getters/setters
}
```
Here it is the isolated code that demonstrates the issue. It works fine with 2.17.3 but doesn't with anything beyond that (2.18, 2.19, 2.20)
```java
JsonMapper objectMapper = JsonMapper.builder()
.constructorDetector(ConstructorDetector.DEFAULT).build();
objectMapper.registerModule(new ParameterNamesModule());
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
Foo foo = objectMapper.readValue("{}", Foo.class);
```
It also fails to deserialize
```
String content = objectMapper.writeValueAsString(new Foo("blah", "qqq"));
Foo foo1 = objectMapper.readValue(content, Foo.class);
```
### Expected behavior
_No response_
### Additional context
_No response_
Seems to have been affected by https://github.com/FasterXML/jackson-databind/issues/4515. Have you found work around yet @oleksiimiroshnyk
Adding @JsonCreator does the trick in the test. The issue is that it is not always possible to do that in source code. In theory I can do instrumentation and it on fly. But I would prefer if this continue working without need of any changes.
So you haven't found one yet.
Here is Jackson-friendly reproduction that indeed works in 2.17 but fails starting 2.18. Although I am not familiar with configurations around constructor. One thing for sure is, we need Mapper-configuration, to meet your requirements (of not touching DTO's)
package com.fasterxml.jackson.databind;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import org.junit.jupiter.api.Test;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
public class JacksonTest extends DatabindTestUtil {
public static class Foo {
private final String id;
private String name;
public Foo(@ImplicitName("id") String id) {
this.id = id;
}
public Foo(@ImplicitName("id") String id, @ImplicitName("name") String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Test
public void testFoo()
throws Exception {
JsonMapper objectMapper = JsonMapper.builder()
.annotationIntrospector(new ImplicitNameIntrospector())
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED).build();
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
String content = objectMapper.writeValueAsString(new Foo("blah", "qqq"));
Foo foo1 = objectMapper.readValue(content, Foo.class);
}
}
I think setup in the test reflects what we have and what spring boot configures OOB
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
.registerModule(new JavaTimeModule());
.registerModule(new ParameterNamesModule());
.registerModule(new Jdk8Module());
I can change objectMapper setup if needed, like a new feature etc...
I can change objectMapper setup if needed, like a new feature etc...
Sadly that is one thing that I do not know :-/
- What to configure
- Or the possibility of it
@cowtowncoder might?
Any chance that the final field mutator config mentioned in #5291 might help as a workaround?
Jackson not being able to find the public Foo(String id, String name) constructor in @oleksiimiroshnyk 's example does seem like a bug to me though.
public class Foo {
private final String id;
private String name;
!!!! NO DEFAULT CONSTRUCTOR !!!!!
public Foo(String id) {
this.id = id;
}
public Foo(String id, String name) {
this.id = id;
this.name = name;
}
Ok, given example should NOT have worked as is. So unfortunately 2.18+ behavior seems as-intended.
So: although Jackson does allow Creator auto-detection in some cases, it should not work here because there are 2 alternatives: this is not allowed (i.e. Jackson will not try to guess what to use).
(this is assuming parameter names would be available with jackson-module-parameter-names).
To make this work in 2.18+ something needs to be added:
- Mark one of constructors with
@JsonIgnore(leaving one to be used) OR - Mark intended constructor with
@JsonCreatorOR - Add default constructor and rely on
MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORSto use Fields for binding values.
Behavior in 2.17.3 or before is odd; I don't know why Creator auto-detection would select one of constructors. That seems like a bug (unfortunate one). But intended rules are clear.
... speculating a bit; I think this:
public Foo(String id) { ... }
may have been detected as legacy delegating Creator (that is, binding from JSON String, not Object). Leaving just one visible (public) constructor to auto-detect.
Be that as it may, the new behavior is the intended one and I don't think it should be changed.
@cowtowncoder jackson-module-scala works pretty well without generally requiring JsonCreator annotations. One thing that works fairly well for me in jackson-module-scala's introspection code is defaulting to the constructor with the most params. I'm sure this can cause its own problems but it seems to work fairly well in jackson-module-scala. Going forward, I would expect Java records to eventually predominate for serde use cases and if we can at least support records where you rarely need to specify JsonCreator annotations that would be great.
Based on historical issues, my tolerance for logic changes in this area is bit limited. There are complexities with equal number of parameters etc.
However, if this was added as an opt-in MapperFeature that could work (earliest in 3.1)?
So maybe worth filing an RFE issue with clear ask.