Search before asking
- [X] I searched in the issues and found nothing similar.
Describe the bug
Deserialzing a JSON with a duplicated property fails for a Java Record with a single property.
Version Information
2.17.2
Reproduction
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
class JacksonDeserializationTest {
record MyRecord(String first) {
}
@Test
void test() throws Exception {
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"first": "test@example.com",
"first": "test@example.com"
}
""";
var value = mapper.readValue(json, MyRecord.class);
System.out.println(value);
}
}
Produces the following error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No fallback setter/field defined for creator property 'first' (through reference chain: JacksonDeserializationTest$MyRecord["first"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.deser.CreatorProperty._reportMissingSetter(CreatorProperty.java:354)
at com.fasterxml.jackson.databind.deser.CreatorProperty._verifySetter(CreatorProperty.java:341)
at com.fasterxml.jackson.databind.deser.CreatorProperty.deserializeAndSet(CreatorProperty.java:270)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:273)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:470)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)
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)
at JacksonDeserializationTest.test(JacksonDeserializationTest.java:20)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Expected behavior
Deserialization works, or a proper exception describing the problem (e.g. "duplicate property") is thrown.
Additional context
Workaround: Add a 2nd dummy property to the Java Record
Ofc the duplicate property in the JSON is not the default. It was detected during a penetration test when testing for parameter pollution vulnerabilities.
Comment From: cowtowncoder
Thank you for reporting this issue. I think I know why it occurs, technically speaking, but not sure how to address it.
But in the meantime... to fail on duplicate property values, generally, it's enough to enable
StreamReadFeature.STRICT_DUPLICATE_DETECTION
on JsonFactory
used to construct ObjectMapper
/ JsonMapper
.
Comment From: yihtserns
Quoting @cowtowncoder from https://github.com/FasterXML/jackson-core/issues/60:
...JSON specification does not make duplicate Object values strictly illegal (behavior is undefined I think)...
...checking for duplicates adds non-trivial amount of cost which for valid content is pure overhead...so...add a feature to let users request that duplicate detection is enabled...
Comment From: sseelmann
Thanks for the suggestions, but I think I won't enable the STRICT_DUPLICATE_DETECTION
feature because
* of the processing overhead and
* we use spring boot so the object mapper is "managed" by it, even thought it's probably configurable, but I'd prefer to use the default config
Comment From: tgyurci
I ran into the same error when tried to use @JsonAlias
on a records's member. Both cases throw the same exception:
@Test
void testRecordAliasLast() throws Exception {
final var r = objectMapper.readValue("""
{"a": "A", "b": "B"}""", R.class);
assertThat(r).isEqualTo(new R("B"));
}
@Test
void testRecordAliasFirst() throws Exception {
final var r = objectMapper.readValue("""
{"b": "B", "a": "A"}""", R.class);
assertThat(r).isEqualTo(new R("A"));
}
public record R(@JsonAlias("b") String a) {
}
No fallback setter/field defined for creator property 'a' (of `R`) (through reference chain: R["a"])
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No fallback setter/field defined for creator property 'a' (of `R`) (through reference chain: R["a"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.deser.CreatorProperty._reportMissingSetter(CreatorProperty.java:358)
at com.fasterxml.jackson.databind.deser.CreatorProperty._verifySetter(CreatorProperty.java:341)
at com.fasterxml.jackson.databind.deser.CreatorProperty.deserializeAndSet(CreatorProperty.java:270)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:273)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:472)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1497)
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:4917)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3828)
Comment From: JooHyukKim
@tgyurci thank you for sharing your case, but is there anything else you are expecting?
Comment From: cowtowncoder
Ideally we should at least give better error message. Good to know of multiple paths that can result in this problem.
Comment From: tgyurci
@tgyurci thank you for sharing your case, but is there anything else you are expecting?
@JooHyukKim I'd expect to work like how it does for normal classes:
public class C {
@JsonAlias("b")
private String a;
}
Comment From: cowtowncoder
Due to implementation I suspect our most practical solution would be just use the first value and ignore the second value (which is different from POJO with setters/fields). This because Constructor-passed values are collected as a set, passed via constructor invocation, and there is no way to mutate values afterward. That should be doable.
An alternative -- which may or may not be practical would be to change construct-based handling to defer call until... hmmh. Yeah, that probably wouldn't work.
But "use first, ignore rest" (or, possibly, "fail on dups", maybe with config setting) is probably achievable.
Comment From: tgyurci
My real world scenario is that I want to migrate a field to a new name thus either the actual property or the alias is present in the JSON. Therefore the preference order is irrelevant in my case.
Comment From: yihtserns
My real world scenario is that I want to migrate a field to a new name thus either the actual property or the alias is present in the JSON. Therefore the preference order is irrelevant in my case.
@tgyurci If you're migrating, that means the JSON would NOT have both the actual property and the alias - which is a case that Records + JsonAlias DO support:
@Test
void testAlias() throws Exception {
final var r = objectMapper.readValue("""{"b": "B"}""", R.class); // alias only
assertThat(r).isEqualTo(new R("B"));
}
@Test
void testActualProperty() throws Exception {
final var r = objectMapper.readValue("""{"a": "A"}""", R.class); // actual property only
assertThat(r).isEqualTo(new R("A"));
}
public record R(@JsonAlias("b") String a) {
}
...yet your original example include BOTH the actual property & alias in the same JSON, which does not fit the word "migration" & "either/or".
Comment From: cowtowncoder
I think one way to catch duplicates for Records would be to add virtual FieldProperty
(or similar) that would throw suitable exception. But that could be confusing for the Alias use case.