There are two problems here. The first problem is based in com.fasterxml.jackson.databind.deser.impl.MethodProperty
not reading the property value if a read method is present. So the deserialization for nested objects always starts with a null
value and therefore constructs a new instance first.
The other problem is handling the collection elements that are already part of the list.
To solve the first problem do the following:
- create an
com.fasterxml.jackson.databind.deser.SettableBeanProperty
implementation that does read the property value if a read method is present and then use that value as the basis for deserialization:
public class DeepUpdatingMethodProperty extends SettableBeanProperty {
private final MethodProperty delegate;
private final Method propertyReader;
public DeepUpdatingMethodProperty(MethodProperty src, Method propertyReader) {
super(src);
this.delegate = src;
this.propertyReader = propertyReader;
}
@Override
public SettableBeanProperty withValueDeserializer(JsonDeserializer<?> deser) {
return new DeepUpdatingMethodProperty((MethodProperty) delegate.withValueDeserializer(deser), propertyReader);
}
@Override
public SettableBeanProperty withName(PropertyName newName) {
return new DeepUpdatingMethodProperty((MethodProperty) delegate.withName(newName), propertyReader);
}
@Override
public SettableBeanProperty withNullProvider(NullValueProvider nva) {
return new DeepUpdatingMethodProperty((MethodProperty) delegate.withNullProvider(nva), propertyReader);
}
@Override
public AnnotatedMember getMember() {
return delegate.getMember();
}
@Override
public <A extends Annotation> A getAnnotation(Class<A> acls) {
return delegate.getAnnotation(acls);
}
@Override
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException {
deserializeSetAndReturn(p, ctxt, instance);
}
@Override
public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance)
throws IOException {
if (instance != null && !p.hasToken(JsonToken.VALUE_NULL)) {
Object readValue;
try {
readValue = readValueFromInstance(instance);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
_throwAsIOE(p, e, instance);
return instance;
}
if (readValue != null) {
return deepUpdateDeserializeSetAndReturn(p, ctxt, instance, readValue);
}
}
return delegate.deserializeSetAndReturn(p, ctxt, instance);
}
private Object deepUpdateDeserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance,
Object readValue) throws IOException, JacksonException, JsonMappingException {
Object value;
if (_valueTypeDeserializer == null) {
value = _valueDeserializer.deserialize(p, ctxt, readValue);
if (value == null) {
if (NullsConstantProvider.isSkipper(_nullProvider)) {
return instance;
}
value = _nullProvider.getNullValue(ctxt);
}
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer, readValue);
}
return setAndReturn(instance, value);
}
private Object readValueFromInstance(Object instance)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
return propertyReader.invoke(instance);
}
@Override
public void set(Object instance, Object value) throws IOException {
delegate.set(instance, value);
}
@Override
public Object setAndReturn(Object instance, Object value) throws IOException {
return delegate.setAndReturn(instance, value);
}
}
- Use a
com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
to modify the properties of a com.fasterxml.jackson.databind.deser.BeanDeserializer
to use the above implementation if a property read method is present
public class DeepUpdatingDeserializerModifier extends BeanDeserializerModifier {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
if(deserializer instanceof BeanDeserializer beanDeserializer) {
enableDeepUpdateForReadableProperties(beanDesc, beanDeserializer);
}
return deserializer;
}
private void enableDeepUpdateForReadableProperties(BeanDescription beanDesc, BeanDeserializer beanDeserializer) {
Iterator<SettableBeanProperty> iter = beanDeserializer.properties();
while(iter.hasNext()) {
SettableBeanProperty property = iter.next();
if (property instanceof MethodProperty methodProperty) {
Method propertyReader = getPropertyReader(methodProperty, beanDesc);
if (propertyReader != null) {
DeepUpdatingMethodProperty adoptedProperty = new DeepUpdatingMethodProperty(methodProperty, propertyReader);
beanDeserializer.replaceProperty(methodProperty, adoptedProperty);
}
}
}
}
private static Method getPropertyReader(MethodProperty src, BeanDescription beanDesc) {
BeanInfo beanInfo;
Class<?> propertyRawClass = beanDesc.getBeanClass();
try {
beanInfo = Introspector.getBeanInfo(propertyRawClass);
} catch (IntrospectionException e) {
throw new IllegalStateException(MessageFormat.format("Could not introspect {0}.", propertyRawClass), e);
}
return Arrays.asList(beanInfo.getPropertyDescriptors()).stream()
.filter(e -> Objects.equals(src.getName(), e.getName())).map(PropertyDescriptor::getReadMethod)
.findFirst().orElse(null);
}
}
Note that this will only work for method properties and not for constructor properties for example.
Regarding the second problem I changed collection deserialization to something different so that instead of a plain json representation I have something like this to deserialize the collection from:
{
"someListProperty": {
"collectionModifications": [{
"_type": "removeIf",
"predicate": {
"_type": "hasPropertyWithValue",
"property": "foo",
"valueMatcher": {
"_type": "or",
"matchers": [{
"_type": "equalTo",
"value": "foobar"
}, {
"_type": "startsWith",
"prefix": "foo."
}
]
}
}
}, {
"_type": "add",
"elements": [{
"foo": "bar"
}, {
"foo": "baz"
}
]
}
]
}
}
The above example will remove values with property foo
equal to foobar
or starting with foo.
from the list first and then add the values specified in elements
. You could implement your own com.fasterxml.jackson.databind.deser.std.CollectionDeserializer
based on your needs. However I found that working with maps instead of lists is easier in such cases but I hadn't such a use case in my example yet. In your example you could use the name
property as the map key.