As an alternative to Łukasz Mączka's solution, if you want to still receive an instance of Foo in your Controller, you can create a Deserializer class, using reflection for a generic approach.
import com.fasterxml.jackson.databind.JsonDeserializer;
public class FooDeserializer extends JsonDeserializer<Foo> {
@Autowired
private FooService service;
@Override
public Foo deserialize(JsonParser p, DeserializationContext ctxt) {
ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
JsonNode patchNode = objectMapper.readTree(p);
// If editting, start with entity stored on db, otherwise create a new one
Long id = extractEntityIdFromPathVariable();
Foo instance = id!=null ? service.findById(id) : new Foo();
if (instance==null) throw new EntityNotFoundException();
// For every field sent in the payload... (fields not sent are not considered)
patchNode.fields().forEachRemaining(entry -> {
String fieldName = entry.getKey();
JsonNode valueNode = entry.getValue();
Field f = Foo.class.getDeclaredField(fieldName);
if (valueNode instanceof NullNode) { // If value of field in the payload is null, set it to null in the instance
// THIS IS THE SOLUTION TO YOUR PROBLEM :)
f.set(instance, (Object)null);
} else { // Otherwise, map it to the correspondant type and set it in the instance
f.set(instance, new ObjectMapper().treeToValue(valueNode, f.getType()));
}
});
return foo;
}
private Long extractEntityIdFromPathVariable() {
// Extract the entity ID from the URL path variable
String pathInfo = request.getRequestURI();
if (pathInfo != null) {
String[] pathSegments = pathInfo.split("/");
if (pathSegments.length > 1) {
try {
return Long.parseLong(pathSegments[pathSegments.length-1]);
} catch (NumberFormatException e) {
// Handle the case where the path variable is not a valid I
}
}
}
// Handle the case where the ID is not found in the path
return null;
}
}
There are some exceptions you need to handle here. I did not include them to simplify the code.
Then, define it as the default serializer in your Foo class:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize(using = FooDeserializer.class)
public class Foo {
protected Date deleteDate;
...
}
Make sure the fields you are editing are public or protected (in the last, the model and the deserializer classes must be in the same package). Otherwise the set won't work.
As an alternative, you can get the java.lang.reflect.Method through Foo.class.getMethod(), using String.format() to merge fieldName with the method name (probably prepending "set" to it).
Finally, in your controller method you just have to call the save method on the repository, as the deserializer is called before the controller method and so the argument you get there already has the "cleaned" version. :)
public FooController {
@PatchMapping("{id}")
public Foo fooEdit(@RequestBody Foo foo) {
return fooRepository.save(foo);
}
}