Reputation: 781
I want to set null value to entity by sending null request.
For example:
PATCH: "{deleteDate: null}" to http://localhost/api/entity/1
But it doesn't work.
I found here information how PATCH requests processed:
- An new instance of Foo is created
- Foo is populated with all values that have been sent with the request
- The Foo entity with the id provided by the URI is loaded
- All properties that differ between the two objects are copied from the new Foo to the persisted Foo, unless the value is null in the new Foo.
Do I understand correctly that it is impossible to set value to NULL with PATCH request to spring-data-rest service API?
Upvotes: 14
Views: 12863
Reputation: 3
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
orprotected
(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
throughFoo.class.getMethod()
, usingString.format()
to mergefieldName
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);
}
}
Upvotes: 0
Reputation: 81
It is possible to achieve that using Map<String,Object> as a request body a then you can extract data using forEach on map.
Example Controller:
@PatchMapping("api/entity/{entityId}")
ResponseEntity<EntityDto> modifyEntity(@PathVariable UUID entityId,
@RequestBody Map<String, Object> modifyEntityData) {
EntityDto modifiedEntity = entityService
.modifyEntity(entityId, modifyEntityData);
return ResponseEntity
.status(HttpStatus.OK)
.body(modifiedEntity );
}
Example Service:
@Transactional
public EntityDto modifyEntity(UUID entityId, Map<String, Object> modifyEntityData) {
// I assume you have a method that returns Entity by its Id in service
Entity entityToModify = findEntity(entityId);
entityToModify = applyChangesToEntity(entityToModify, modifyEntityData);
// And mapper that returns dto (ofc optional)
return EntityMapper.mapEntityToDto(entityToModify);
}
private EntityDto applyChangesToEntity(Entity exisistingEntity,
Map<String, Object> modifyEntityData) {
modifyEntityData.forEach((key, value) -> {
switch (key) {
case "deleteDate" -> {
// Check if null
if (value == null) {
exisistingEntity.setDeleteDate(null);
break;
}
// If value is present you can do w/e you want
if (validateValueOk(value)) {
exisistingEntity.setDeleteDate(value.toString());
} else {
throw new RuntimeException("DeleteDate must be valid or other message");
}
}
// HERE MORE CASES
default -> throw new RuntimeException("Message");
}
});
return exisistingEntity;
}
Upvotes: 0
Reputation: 11
All 3 options from egorlitvinenko's answer will solve the described problem but will have another one:
PATCH request
would be "nullified" too.Seems like spring-data-rest, issue-345 was already fixed in v2.2.x
.
Upvotes: 1
Reputation: 2776
In Spring context null values in PATCH method means that there are no changes. If you want write null values you can
1) use PUT method;
2) implement your own DomainObjectMerger class, in which you can extend method merge with condition like
sourceValue != targetValue;
3) use DomainObjectMerger.NullHandlingPolicy configuration.
Depends on your Spring Data REST version.
Upvotes: 10