Search before asking
- [X] I searched in the issues and found nothing similar.
Describe the bug
If ParameterNamesModule is enabled, the constructor is called with a null value which causes deserialization to fail if the constructor has a null check. However, if the null value is ignored the resulting deserialized object has a non-null value.
Version Information
2.15.2, 2.16.1
Reproduction
Given a class:
import java.beans.ConstructorProperties;
public final class TestClass {
@ConstructorProperties("value")
public TestClass(String somethingElse) {
if (somethingElse == null) {
System.out.println("Constructor called with null value");
}
this.value = somethingElse;
}
private final String value;
public String getValue() {
return this.value;
}
}
And a unit test:
import static org.assertj.core.api.BDDAssertions.then;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
class TestCase {
@Test
void test() throws Exception {
// Given
var object = new TestClass("test");
var mapper = new ObjectMapper();
mapper.registerModule(new ParameterNamesModule());
// When
var toString = mapper.writeValueAsString(object);
var fromString = mapper.readValue(toString, TestClass.class);
// Then
then(fromString.getValue()).isEqualTo("test");
}
}
The test passes, but it prints
Constructor called with null value
If my class had a null check in the constructor, then an exception would be thrown even though the end result of the deserialization would have been correct and resulted in a non-null value.
This issue disappears if I remove the ParameterNamesModule, rename the constructor parameter to match the field name, or add @JsonProperty("value")
.
Expected behavior
Additional context
No response
Comment From: pjfanning
try adding the JsonCreator annotation to your constructor
https://javadoc.io/static/com.fasterxml.jackson.core/jackson-annotations/2.16.1/com/fasterxml/jackson/annotation/JsonCreator.html
Comment From: lbenedetto
That makes things worse:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `TestClass` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"value":"test"}"; 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:4899)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3846)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3814)
Comment From: cowtowncoder
First of all, this does sound like a flaw of some kind, possibly conflict between information provided by jackson-databind's own handler for @ConstructorProperties
vs parameter name module.
So thank you for reporting this issue.
Note this is the ambiguous case of 1-arg constructor, which could be seen as either delegating (bind value into parameter type) or properties-based (bind an Object property into value), depending; heuristics are used unless annotations are found. This is why workarounds listed work: they make heuristic work; looks like "delegating" mode is otherwise used.
So you probably would need no explicitly specify mode
like:
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
to make that approach work. But that should not be necessary, @ConstructorProperties
should be enough.
Finally, it is possible Lombok might be contributing to the problem since it does bytecode manipulation, possibly copying (or not copying) some of annotations. It would be good to know if the issue can be reproduced without Lombok (our unit tests cannot use Lombok so for regression test(s) removal would be needed).
Comment From: cowtowncoder
Quick note: I may need to move this issue to jackson-modules-java8
if test requires ParameterNamesModule
: jackson-databind
cannot depend on that module, even via tests (but opposite is fine; modules depend on databind).
Comment From: lbenedetto
I've updated my example test to avoid lombok, and the issue still occurs.
I've tested adding @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
, and it doesn't do anything on its own, one of the other workarounds is still required. (eg, adding @JsonProperty("value")
which was already working on its own without @JsonCreator
)
Setting the mode to DELEGATING
does cause the deserialization to fail, so I think it was already using PROPERTIES
mode by default via heuristics.
I think you should go ahead and move the issue since I've been unable to reproduce the erroneous behavior without ParameterNamesModule
.
Comment From: lbenedetto
In AnnotationIntrospectorPair::findImplicitPropertyName
the primary (ParameterNamesAnnotationIntrospector
) finds "somethingElse"
and the secondary (JacksonAnnotationIntrospector
) would find "value"
if it was called (which it isn't because the result from the primary is not null).
It seems backwards to me for the ParameterNamesAnnotationIntrospector
to be the primary since as far as I can tell it only looks at the JVM name is (java.lang.reflect.Executable::getParameters
), whereas the JacksonAnnotationIntrospector
looks at annotations that explicitly override this value.
Comment From: cowtowncoder
Ok, so the problem really comes down to couple of things that seem unfortunate, but also something not easy to change wrt backwards-compatibility.
First, @ConstructorProperties
provided names are consider "implicit" (and not explicitly annotated) -- if they were explicit, they would "win" over other implicit names. Parameter names should be implicit for sure.
Second: in case of both annotation introspectors (databind's JacksonAnnotationIntrospector
), ParameterNamesModule
registers its introspector using "insert", which means it has higher precedence.
There is no simple configuration setting to change that, although you could definitely create and register (with SetupContext.appendAnnotationIntrospector()
from Module
) introspector, ParameterNamesAnnotationIntrospector
.
That would resolve the problem.
Comment From: cowtowncoder
One thing that would be doable for Jackson 2.18 would be to add Yet Another Configuration setting to JacksonAnnotationIntrospector
to make @ConstructorProperties
- provided names to be explicit, instead of implicit.
I agree that this would usually be the most reasonable choice. But I also think they were some earlier (multiple years ago) discussions about some code generators adding this annotation automatically, in which case it'd be more like implicit (default) names; and because of this default was made implicit, I think.
Regardless, as long as configurability exists it should be fine to allow making these names be considered explicit.
Comment From: cowtowncoder
Turns out this actually works in 3.0 due to higher priority for implicit names from @ConstructorProperties
.
But also: very difficult to fix in 2.x.
Will do PR for added test in 3.0, then mark as fixed.