Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm trying to distinguish between null values and not provided values when partially updating an entity with PUT request method in Spring Rest Controller.

Consider the following entity, as an example:

@Entity private class Person {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      /* let's 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> { } 

The DTO I use:

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 behaviour: update firstName= "John" (leave lastName unchanged).

Request with null property

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

Expected behaviour: update firstName="John" and set lastName=null.

I cannot distinguish between these two cases, sincelastName in the DTO is always set to null by Jackson.

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

like image 620
Danilo Cianciulli Avatar asked Jul 17 '16 18:07

Danilo Cianciulli


People also ask

How do I ignore NULL values in JSON response spring boot?

You can ignore null fields at the class level by using @JsonInclude(Include. NON_NULL) to only include non-null fields, thus excluding any attribute whose value is null. You can also use the same annotation at the field level to instruct Jackson to ignore that field while converting Java object to json if it's null.

What is the difference between spring controller and rest controller?

@Controller is used to mark classes as Spring MVC Controller. @RestController annotation is a special controller used in RESTful Web services, and it's the combination of @Controller and @ResponseBody annotation. It is a specialized version of @Component annotation.

What is the difference between normal controller and RestController?

The @Controller is a common annotation which is used to mark a class as Spring MVC Controller while the @RestController is a special controller used in RESTFul web services and the equivalent of @Controller + @ResponseBody .

What is rest controller in Spring MVC?

Spring RestController annotation is a convenience annotation that is itself annotated with @Controller and @ResponseBody . This annotation is applied to a class to mark it as a request handler. Spring RestController annotation is used to create RESTful web services using Spring MVC.


1 Answers

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 null, and would be ignored by the @JsonInclude annotation. Otherwise, if implicitly set in the request object, firstName would not be null, but firstName.get() would be. I found this browsing the solution @laffuste linked to a little lower down in a different comment (garretwilson's initial comment saying it didn't work turns out to work).

You can also map the DTO to the Entity with Jackson's 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 ...     } } 

Validating a DTO using java.util.Optional is a little different as well. It's documented here, but 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 null if PersonDTO is validated.

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

Also might be worth mentioning the use of Optional seems to be highly debated, and as of writing Lombok's maintainer(s) won't support it (see this question for example). This means using lombok.Data/lombok.Setter on a class with Optional fields with constraints doesn't work (it attempts to create setters with the constraints intact), so using @Setter/@Data causes an exception to be thrown as both the setter and the member variable have constraints set. It also seems better form to write the 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... } 
like image 121
zbateson Avatar answered Sep 20 '22 12:09

zbateson