1

I have a problem with Objectmapper.readerforupdating using different write permissions with the help of views, it is working fine on main entity but not on nested objects. I have the following example:

public class A {

    @JsonView(value={WritePermission.Admin})
    private String name;
    
    @JsonView(value={WritePermission.User})
    private String property;
    
    @JsonView(value={WritePermission.User})
    private List<B> list;
}

public class B {
    @JsonView(value={WritePermission.Admin})
    private String name;
    
    @JsonView(value={WritePermission.User})
    private String property;
    
}

public class WritePermission {
    public WritePermission() {
    }
   
     public static class Admin extends WritePermission.User {
        public Admin() {
        }
    }

    public static class User {
        public User() {
        }
    }
}    

For deserialization I use this: objectMapper.readerForUpdating(initialEntityOfAClass).withView(WritePermission.User.class).forType(A.class).readValue(json) and also tried with this but I get the same result: ObjectReader objectReader = objectMapper.readerForUpdating(initialEntityOfAClass);

When I want to deserialize a json with a User write role, I would like to overwrite only the attributes where I have permission, this works fine for attributes from class A (in the name attribute remains the old value cuz I do not have the rights to update, property attribute is updated) but it does not work for the list of B items - instead of changing the B objects just like for A (name remains old value, property is updated from json) creates new objects for the list of B and name remains null because as a User I do not have a permission to write values to name attribute. If I set @JsonMerge on the B list, instead of merge I am getting the old (the ones where the name is already set but property not changed) and the newly created objects (the ones where the property changed but with name=null) in one list... Can anyone help me here, please?

Eni
  • 13
  • 3

1 Answers1

0

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.

SpaceTrucker
  • 13,377
  • 6
  • 60
  • 99