Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
In Jackson 2, a mixin for a getter configured as a property can declare deserialization customizations such as @JsonDeserializer on the redeclared getter. In Jackson 3, that customization is ignored, and the property is entirely ignored if no setter is declared at all.
It looks like in 2, a field backing a getter will still be considered for deserialization, despite no explicit setter being exposed by the type to deserialize into, and customizations for that can be applied to the getter (through a mixin, potentially).
Is there a way to set up the old behavior through configuration without declaring a setter? I've tried using @JsonProperty(access = READ_WRITE) on the getter, but that still leaves me with the property not even considered for deserialization.
Version Information
3.0 RC8 (RC1 doesn't work either)
Reproduction
Works on Jackson 2
public class CustomizationTest {
@Test
void usesFieldForDeserialization() throws Exception {
ObjectMapper mapper = JsonMapper.builder().addMixIn(Sample.class, SampleMixin.class).build();
Sample result = mapper.readValue("{\"values\" : [ \"Value\" ] }", Sample.class);
assertThat(result.getValues()).isInstanceOfSatisfying(Wrapper.class, it -> {
assertThat(it.values).containsExactly("FIXED!");
});
}
public static class Sample {
private final List<String> values;
public Sample() {
this.values = new ArrayList<>();
}
public Wrapper getValues() {
return new Wrapper(values);
}
}
public static class Wrapper {
private final List<String> values;
Wrapper(List<String> values) {
this.values = values;
}
}
public static abstract class SampleMixin extends Sample {
@Override
@JsonDeserialize(using = CustomDeserializer.class)
public abstract Wrapper getValues();
}
public static class CustomDeserializer extends ContainerDeserializerBase<List<String>> {
public CustomDeserializer() {
super(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, String.class));
}
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
@Override
public List<String> deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
return List.of("FIXED!");
}
}
}
In Jackson 3 an even simpler case, not involving a custom collection wrapper doesn't work.
public class CustomizationTest {
@Test
void usesFieldForDeserialization() {
ObjectMapper mapper = JsonMapper.builder().addMixIn(Sample.class, SampleMixin.class).build();
Sample result = mapper.readValue("{\"value\" : [ \"Value\" ] }", Sample.class);
assertThat(result.getValue()).containsExactly("FIXED!");
}
public static class Sample {
private final List<String> values;
public Sample() {
this.values = new ArrayList<>();
}
public List<String> getValues() {
return values;
}
}
public static abstract class SampleMixin extends Sample {
@Override
@JsonDeserialize(using = CustomDeserializer.class)
public abstract List<String> getValue();
}
public static class CustomDeserializer extends ContainerDeserializerBase<List<String>> {
private static final TypeFactory FACTORY = TypeFactory.createDefaultInstance();
public CustomDeserializer() {
super(FACTORY.constructCollectionLikeType(List.class, String.class));
}
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
@Override
public List<String> deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
return List.of("FIXED!");
}
}
}
Expected behavior
I need a migration path that allows me to customize the deserialization of the field via the accessor method via a mixin.
Additional context
No response
Comment From: pjfanning
A lot of mapper settings have changed in terms of what they default to. You can make Jackson 3 use settings that approximate the Jackson 2 defaults. Example: https://github.com/FasterXML/jackson-databind/blob/3.x/src/test/java/tools/jackson/databind/json/JsonMapperBuilderTest.java#L24
Comment From: odrotbohm
I have no influence on the settings that users define for their ObjectMapper in general as I only provide model objects and JacksonModules that customize their rendering.
Comment From: cowtowncoder
Would need actual reproduction: description useful but not sufficient.
EDIT : reproduction seems to be added
Comment From: cowtowncoder
I wonder if this could actually be due to reliance on MapperFeature.USE_GETTERS_AS_SETTERS, default of which did change in 3.0 (to false).
Especially given that there's naming discrepancy:
public static class Sample {
private List<String> values;
public List<String> getValue() {
return values;
}
}
// ....
Sample result = mapper.readValue("{\"value\" : [ \"Value\" ] }", Sample.class);
between getValue() -> property "value" and values Field.
But if so it's odd that smaller case would work at all (and not fail on "No property "value"" or such)
Comment From: odrotbohm
I've set up a proper reproducer locally, and it turns out it's the default change in MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS that causes the field behind the property not to be registered in Jackson 3, which ultimately causes the value not to be serialized into it.
I'll have to make up my mind about how to make sure that this still works without the mapping setup not entirely being in our control, but as it stands today, this would break on Jackson 2, too, if folks were to customize the mapper to disable that feature. Feel free to close as works as designed.
Comment From: cowtowncoder
@cowtowncoder Ah! Yes, that makes sense actually. Let me think about this a bit.
Comment From: cowtowncoder
Ok, yes, I think behavior makes sense as described, and I don't think it can be changed from Jackson side.
So, to summarize: behavior is due to no mutator ("setter") found as Field values is final -- due to change of MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS to false in 3.0 -- and as such mix-in has no effect.
Will close.