How to distinguish between null and not provided values โ€‹โ€‹for partial updates in Spring Rest Controller

I am trying to distinguish between null values โ€‹โ€‹and not provided values โ€‹โ€‹when partially updating an object using the PUT request method in Spring Rest Controller.

As an example, consider the following entity:

@Entity private class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /* let assume the following attributes may be null */ private String firstName; private String lastName; /* getters and setters ... */ } 

My Person Repository (Spring Data):

 @Repository public interface PersonRepository extends CrudRepository<Person, Long> { } 

Used DTO:

 private class PersonDTO { private String firstName; private String lastName; /* getters and setters ... */ } 

My Spring RestController:

 @RestController @RequestMapping("/api/people") public class PersonController { @Autowired private PersonRepository people; @Transactional @RequestMapping(path = "/{personId}", method = RequestMethod.PUT) public ResponseEntity<?> update( @PathVariable String personId, @RequestBody PersonDTO dto) { // get the entity by ID Person p = people.findOne(personId); // we assume it exists // update ONLY entity attributes that have been defined if(/* dto.getFirstName is defined */) p.setFirstName = dto.getFirstName; if(/* dto.getLastName is defined */) p.setLastName = dto.getLastName; return ResponseEntity.ok(p); } } 

Request with missing property

 {"firstName": "John"} 

Expected behavior: update firstName= "John" (leave lastName unchanged).

Request with a null property

 {"firstName": "John", "lastName": null} 

Expected Behavior: Update firstName="John" and set lastName=null .

I cannot distinguish between these two cases, since lastName in DTO is always set to null Jackson.

Note: I know that the best REST methods (RFC 6902) recommend using PATCH instead of PUT for partial updates, but in my specific scenario I need to use PUT.

+16
java json rest jackson spring-mvc
source share
5 answers

Actually, if you ignore the check, you can solve your problem as follows.

  public class BusDto { private Map<String, Object> changedAttrs = new HashMap<>(); /* getter and setter */ } 
  • First write a superclass for your dto, such as BusDto.
  • Second, change your dto to extend the super class, and change dto to put the name and value of the attribute in changedAttrs (beacause spring will call the set when the attribute is no matter null or not null).
  • Thirdly, bypassing the map.
+2
source share

Use boolean flags as Jackson recommends .

 class PersonDTO { private String firstName; private boolean isFirstNameDirty; public void setFirstName(String firstName){ this.firstName = firstName; this.isFirstNameDirty = true; } public void getFirstName() { return firstName; } public boolean hasFirstName() { return isFirstNameDirty; } } 
+6
source share

I tried to solve the same problem. It was pretty easy for JsonNode to use JsonNode as a DTO. This way you only get what is sent.

You will need to write the MergeService yourself, which does the actual work, similar to BeanWrapper. I did not find an existing structure that can do exactly what is needed. (If you use only Json requests, you can use the Jacksons readForUpdate method.)

In fact, we use a different type of node, because we need the same functionality from the "standard forms" and other service calls. In addition, modifications must be applied within a transaction within what is called an EntityService .

This MergeService , unfortunately, will become quite complicated, since you will have to process the properties, lists, sets and maps yourself :)

The most problematic for me was the difference between changes to a list / set item and modifications or replacements of lists / sets.

And also the check will not be simple, since you need to check some properties against another model (JPA entities in my case)

EDIT - code code (pseudocode):

 class SomeController { @RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public void save( @PathVariable("id") final Integer id, @RequestBody final JsonNode modifications) { modifierService.applyModifications(someEntityLoadedById, modifications); } } class ModifierService { public void applyModifications(Object updateObj, JsonNode node) throws Exception { BeanWrapperImpl bw = new BeanWrapperImpl(updateObj); Iterator<String> fieldNames = node.fieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); Object valueToBeUpdated = node.get(fieldName); Class<?> propertyType = bw.getPropertyType(fieldName); if (propertyType == null) { if (!ignoreUnkown) { throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass()); } } else if (Map.class.isAssignableFrom(propertyType)) { handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects); } else if (Collection.class.isAssignableFrom(propertyType)) { handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects); } else { handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects); } } } } 
+1
source share

Another option is to use java.util.Optional.

 import com.fasterxml.jackson.annotation.JsonInclude; import java.util.Optional; @JsonInclude(JsonInclude.Include.NON_NULL) private class PersonDTO { private Optional<String> firstName; private Optional<String> lastName; /* getters and setters ... */ } 

If firstName is not set, the value is zero and will be ignored by the @JsonInclude annotation. Otherwise, if implicitly set in the request object, firstName will not be null, and firstName.get () will be. I found this by looking at @laffuste's solution related to a slightly lower one in another comment (garretwilson's initial comment, saying that it didn't work, turns out to work).

You can also map the DTO to Entity using Jackson ObjectMapper, and it will ignore properties that were not passed in the request object:

 import com.fasterxml.jackson.databind.ObjectMapper; class PersonController { // ... @Autowired ObjectMapper objectMapper @Transactional @RequestMapping(path = "/{personId}", method = RequestMethod.PUT) public ResponseEntity<?> update( @PathVariable String personId, @RequestBody PersonDTO dto ) { Person p = people.findOne(personId); objectMapper.updateValue(p, dto); personRepository.save(p); // return ... } } 

DTO validation using java.util.Optional is also slightly different. This is documented here , but it took me a while to find:

 // ... import javax.validation.constraints.NotNull; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; // ... private class PersonDTO { private Optional<@NotNull String> firstName; private Optional<@NotBlank @Pattern(regexp = "...") String> lastName; /* getters and setters ... */ } 

In this case, firstName may not be set at all, but if set, may not be set to zero if PersonDTO is checked.

 //... import javax.validation.Valid; //... public ResponseEntity<?> update( @PathVariable String personId, @RequestBody @Valid PersonDTO dto ) { // ... } 

It may also be worth mentioning that the use of Optional seems highly controversial, and at the time of writing, the maintainer (s) of Lombok did not support it (see this question, for example ). This means that using lombok.Data/lombok.Setter in a class with Optional constrained fields does not work (it tries to create setters with constant constraints), so using @ Setter / @ Data raises an exception, since both setter and member variable have established restrictions. It also seems that it is better to write Setter without an optional parameter, for example:

 //... import lombok.Getter; //... @Getter private class PersonDTO { private Optional<@NotNull String> firstName; private Optional<@NotBlank @Pattern(regexp = "...") String> lastName; public void setFirstName(String firstName) { this.firstName = Optional.ofNullable(firstName); } // etc... } 
+1
source share

It may be too late for an answer, but you could:

  • By default, do not remove the null values. Provide an explicit list using the query parameters which fields you want to cancel. That way, you can still send JSON that matches your entity, and have the flexibility to override fields when you need to.

  • Depending on your use case, some endpoints may explicitly treat all null values โ€‹โ€‹as incomplete operations. A bit dangerous to fix, but in some cases it may be an option.

0
source share

All Articles