Spring MVC PATCH method: partial updates

JavaJsonSpringRestSpring Mvc

Java Problem Overview


I have a project where I am using Spring MVC + Jackson to build a REST service. Let's say I have the following java entity

public class MyEntity {
	private Integer id;
	private boolean aBoolean;
	private String aVeryBigString;
	//getter & setters
}

Sometimes, I just want to update the boolean value, and I don't think that sending the whole object with its big string is a good idea just to update a simple boolean. So, I have considered using the PATCH HTTP method to only send the fields that need to be updated. So, I declare the following method in my controller:

@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
	//calling a service to update the entity
}

The problem is: how do I know which fields need to be updated? For instance, if the client just wants to update the boolean, I will get an object with an empty "aVeryBigString". How am I supposed to know that the user just wants to update the boolean, but does not want to empty the string?

I have "solved" the problem by building custom URLs. For instance, the following URL: POST /myentities/1/aboolean/true will be mapped to a method that allows to only update the boolean. The problem with this solution is that it is not REST compliant. I don't want to be 100% REST compliant, but I do not feel comfortable with providing a custom URL to update each field (especially given that it causes problems when I want to update several fields).

Another solution would be to split "MyEntity" into multiple resources and just update these resources, but I feel like it does not make sense: "MyEntity" is a plain resource, it is not composed of other resources.

So, is there an elegant way of solving this problem?

Java Solutions


Solution 1 - Java

This could be very late, but for the sake of newbies and people who encounter the same problem, let me share you my own solution.

In my past projects, to make it simple, I just use the native java Map. It will capture all the new values including the null values that the client explicitly set to null. At this point, it will be easy to determine which java properties needs to be set as null, unlike when you use the same POJO as your domain model, you won't be able to distinguish which fields are set by the client to null and which are just not included in the update but by default will be null.

In addition, you have to require the http request to send the ID of the record you want to update, and do not include it on the patch data structure. What I did, is set the ID in the URL as path variable, and the patch data as a PATCH body.Then with the ID, you would get first the record via a domain model,then with the HashMap, you can just use a mapper service or utility to patch the changes to the concerned domain model.

Update

You can create a abstract superclass for your services with this kind of generic code, you must use Java Generics. This is just a segment of possible implementation, I hope you get the idea.Also it is better to use mapper framework such as Orika or Dozer.

public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
    @Autowired
    private MapperService mapper;
    
    @Autowired
    private BaseRepo<Entity> repo;

    private Class<DTO> dtoClass;

    private Class<Entity> entityCLass;

    public AbstractService(){
       entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
       dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
    }

    public DTO patch(Long id, Map<String, Object> patchValues) {
        Entity entity = repo.get(id);
        DTO dto = mapper.map(entity, dtoClass);
        mapper.map(patchValues, dto);
        Entity updatedEntity = toEntity(dto);
        save(updatedEntity);
        return dto;
    }
}

Solution 2 - Java

The correct way to do this is the way proposed in JSON PATCH RFC 6902

A request example would be:

PATCH http://example.com/api/entity/1 HTTP/1.1
Content-Type: application/json-patch+json 

[
  { "op": "replace", "path": "aBoolean", "value": true }
]

Solution 3 - Java

After digging around a bit I found an acceptable solution using the same approach currently used by a Spring MVC DomainObjectReader see also: JsonPatchHandler

import org.springframework.data.rest.webmvc.mapping.Associations

@RepositoryRestController
public class BookCustomRepository {
    private final DomainObjectReader domainObjectReader;
    private final ObjectMapper mapper;

    private final BookRepository repository;


    @Autowired
    public BookCustomRepository(BookRepository bookRepository, 
                                ObjectMapper mapper,
                                PersistentEntities persistentEntities,
                                Associations associationLinks) {
        this.repository = bookRepository;
        this.mapper = mapper;
        this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
    }


    @PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {

        Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
        Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
        repository.save(patched);

        return ResponseEntity.noContent().build();
    }

}

Solution 4 - Java

The whole point of PATCH is that you are not sending the entire entity representation, so I don't understand your comments about the empty string. You would have to handle some sort of simple JSON such as:

{ aBoolean: true }

and apply that to the specified resource. The idea is that what has been received is a diff of the desired resource state and the current resource state.

Solution 5 - Java

Spring does/can not use PATCH to patch your object because of the same problem you already have: The JSON deserializer creates an Java POJO with nulled fields.

That means you have to provide an own logic for patching an entity (i.e. only when using PATCH but not POST).

Either you know you use only non primitive types, or some rules (empty String is null, which does not work for everyone) or you have to provide an additional parameter which defines the overridden values. Last one works fine for me: The JavaScript application knows which fields have been changed and sent in addition to the JSON body that list to the server. For example if a field description was named to change (patch) but is not given in the JSON body, it was being nulled.

Solution 6 - Java

You could use Optional<> for that:

public class MyEntityUpdate {
    private Optional<String> aVeryBigString;
}

This way you can inspect the update object as follows:

if(update.getAVeryBigString() != null)
    entity.setAVeryBigString(update.getAVeryBigString().get());

If field aVeryBigString is not in the JSON document, the POJO aVeryBigString field will be null. If it is in the JSON document, but with a null value, the POJO field will be an Optional with wrapped value null. This solution allows you to differentiate between "no-update" and "set-to-null" cases.

Solution 7 - Java

I've noticed that many of the provided answers are all JSON patching or incomplete answers. Below is a full explanation and example of what you need with functioning real world code

A full patch function:

@ApiOperation(value = "Patch an existing claim with partial update")
@RequestMapping(value = CLAIMS_V1 + "/{claimId}", method = RequestMethod.PATCH)
ResponseEntity<Claim> patchClaim(@PathVariable Long claimId, @RequestBody Map<String, Object> fields) {

    // Sanitize and validate the data
    if (claimId <= 0 || fields == null || fields.isEmpty() || !fields.get("claimId").equals(claimId)){
		return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Invalid claim object received or invalid id or id does not match object
	}

	Claim claim = claimService.get(claimId);

    // Does the object exist?
	if( claim == null){
		return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Claim object does not exist
	}

	// Remove id from request, we don't ever want to change the id.
    // This is not necessary,
    // loop used below since we checked the id above
	fields.remove("claimId");

	fields.forEach((k, v) -> {
		// use reflection to get field k on object and set it to value v
        // Change Claim.class to whatver your object is: Object.class
		Field field = ReflectionUtils.findField(Claim.class, k); // find field in the object class
		field.setAccessible(true); 
		ReflectionUtils.setField(field, claim, v); // set given field for defined object to value V
	});

	claimService.saveOrUpdate(claim);
	return new ResponseEntity<>(claim, HttpStatus.OK);
}

The above can be confusing for some people as newer devs don't normally deal with reflection like that. Basically, whatever you pass this function in the body, it will find the associated claim using the given ID, then ONLY update the fields you pass in as a key value pair.

Example body:

PATCH /claims/7

{
   "claimId":7,
   "claimTypeId": 1,
   "claimStatus": null
}

The above will update claimTypeId and claimStatus to the given values for claim 7, leaving all other values untouched.

So the return would be something like:

{
   "claimId": 7,
   "claimSrcAcctId": 12345678,
   "claimTypeId": 1,
   "claimDescription": "The vehicle is damaged beyond repair",
   "claimDateSubmitted": "2019-01-11 17:43:43",
   "claimStatus": null,
   "claimDateUpdated": "2019-04-09 13:43:07",
   "claimAcctAddress": "123 Sesame St, Charlotte, NC 28282",
   "claimContactName": "Steve Smith",
   "claimContactPhone": "777-555-1111",
   "claimContactEmail": "[email protected]",
   "claimWitness": true,
   "claimWitnessFirstName": "Stan",
   "claimWitnessLastName": "Smith",
   "claimWitnessPhone": "777-777-7777",
   "claimDate": "2019-01-11 17:43:43",
   "claimDateEnd": "2019-01-11 12:43:43",
   "claimInvestigation": null,
   "scoring": null
}

As you can see, the full object would come back without changing any data other than what you want to change. I know there is a bit of repetition in the explanation here, I just wanted to outline it clearly.

Solution 8 - Java

I fixed the Problem like this, because i can't changed the service

public class Test {

void updatePerson(Person person,PersonPatch patch) {

    for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
        switch (updatedField){

            case firstname:
                person.setFirstname(patch.getFirstname());
                continue;
            case lastname:
                person.setLastname(patch.getLastname());
                continue;
            case title:
                person.setTitle(patch.getTitle());
                continue;
        }
        
    }

}

public static class PersonPatch {

    private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();

    public List<PersonPatchField> updatedFields() {
        return updatedFields;
    }

    public enum PersonPatchField {
        firstname,
        lastname,
        title
    }

    private String firstname;
    private String lastname;
    private String title;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(final String firstname) {
        updatedFields.add(PersonPatchField.firstname);
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(final String lastname) {
        updatedFields.add(PersonPatchField.lastname);
        this.lastname = lastname;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(final String title) {
        updatedFields.add(PersonPatchField.title);
        this.title = title;
    }
}

Jackson called only when values exist. So you can save which setter was called.

Solution 9 - Java

Couldn't you just send an object consisting of the fields that's been updated?

Script call:

var data = JSON.stringify({
                aBoolean: true
            });
$.ajax({
    type: 'patch',
    contentType: 'application/json-patch+json',
    url: '/myentities/' + entity.id,
    data: data
});

Spring MVC controller:

@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
    // updates now only contains keys for fields that was updated
    return ResponseEntity.ok("resource updated");
}

In the controller's pathmember, iterate through the key/value pairs in the updates map. In the example above, the "aBoolean"key will hold the value true. The next step will be to actually assign the values by calling the entity setters. However, that's a different kind of problem.

Solution 10 - Java

I use reflection to solve this problem. The client can send object(e.g in javascript) which would contain all the fields with their respected value. The way I capture the new values in controller:

@PatchMapping(value = "{id}")
public HttpEntity<Map<String, Object>> updatePartial(@PathVariable Integer id, @RequestBody Map<String, Object> data) {
    return ResponseEntity.ok(questionService.updatePartial(id, data));
}

Then into the service implementation we can use the reflection to find if the requested property exists and if it does then update its value.

public Map<String, Object> updatePartial(@NotNull Long id, @NotNull Map<String, Object> data) {

    Post post = postRepository.findById(id);

    Field[] postFields = Post.class.getDeclaredFields();
    HashMap<String, Object> toReturn = new HashMap<>(1);
    for (Field postField : postFields) {
        data.forEach((key, value) -> {
            if (key.equalsIgnoreCase(postField.getName())) {
                try {
                    final Field declaredField = Post.class.getDeclaredField(key);
                    declaredField.setAccessible(true);
                    declaredField.set(post, value);
                    toReturn.put(key, value);
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    log.error("Unable to do partial update field: " + key + " :: ", e);
                    throw new BadRequestException("Something went wrong at server while partial updation");
                }
            }
        });
    }
    postRepository.save(post);

    return toReturn;
}

Spring Data JPA is used here for DB operations.

IF you want to see how I handle this at client(javascript). PATCH call to whatever endpoint with the data as:

{
  voted: true,
  reported: true
}

And then in the response client can verify if the response contains the expected properties. E.g I am expecting all the fields(which I passed as params in PATCH) in response:

if (response.data.hasOwnProperty("voted")){
  //do Something
} else{
  //do something e.g report it
}

Solution 11 - Java

@Mapper(componentModel = "spring")
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface CustomerMapper {
    void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}

public void updateCustomer(CustomerDto dto) {
    Customer myCustomer = repo.findById(dto.id);
    mapper.updateCustomerFromDto(dto, myCustomer);
    repo.save(myCustomer);
}

The drawback of this approach is that we can't pass null values to the database during an update.
See Partial Data Update with Spring Data

  • Solution via json-patch library
  • Solution via spring-data-rest

See https://stackoverflow.com/questions/33288670/custom-spring-mvc-http-patch-requests-with-spring-data-rest-functionality

Solution 12 - Java

Here is an implementation for a patch command using googles GSON.

package de.tef.service.payment;

import com.google.gson.*;

class JsonHelper {
    static <T> T patch(T object, String patch, Class<T> clazz) {
        JsonElement o = new Gson().toJsonTree(object);
        JsonObject p = new JsonParser().parse(patch).getAsJsonObject();
        JsonElement result = patch(o, p);
        return new Gson().fromJson(result, clazz);
    }

    static JsonElement patch(JsonElement object, JsonElement patch) {
        if (patch.isJsonArray()) {
            JsonArray result = new JsonArray();
            object.getAsJsonArray().forEach(result::add);
            return result;
        } else if (patch.isJsonObject()) {
            System.out.println(object + " => " + patch);
            JsonObject o = object.getAsJsonObject();
            JsonObject p = patch.getAsJsonObject();
            JsonObject result = new JsonObject();
            o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey()))));
            return result;
        } else if (patch.isJsonPrimitive()) {
            return patch;
        } else if (patch.isJsonNull()) {
            return patch;
        } else {
            throw new IllegalStateException();
        }
    }
}

The implementation is recursiv to take care about nested structures. Arrays are not merged, because they do not have a key for the merge.

The "patch" JSON is directly converted from String to JsonElement and not to a object to keep the not filled fields apart from the fields filled with NULL.

Solution 13 - Java

This is an old post, but it was still a problem without a good solution for me. Here is what I am leaning towards.

The idea is to leverage the deserialization phase to track what is sent and what is not and have the entity support a way to interrogate the property change state. Here is the idea.

This interface triggers a custom deserialization and forces the bean to carry its state change information

@JsonDeserialize(using = Deser.class)
interface Changes {

    default boolean changed(String name) {
        Set<String> changed = changes();
        return changed != null && changed.contains(name);
    }

    void changes(Set<String> changed);

    Set<String> changes();
}

Here is the deserializer. Once it's invoked, it reverses the deserialization behavior through a mixin. Note, that it will only work when json properties map directly to bean properties. For anything fancier, I think the bean instance could be proxied and setter calls intercepted.

class Deser extends JsonDeserializer<Object> implements ContextualDeserializer {
    private Class<?> targetClass;

    public Deser() {}

    public Deser(Class<?> targetClass) { this.targetClass = targetClass; }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectMapper mapper = (ObjectMapper) p.getCodec();
        TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {
        };
        HashMap<String, Object> map = p.readValueAs(typeRef);
        ObjectMapper innerMapper = mapper.copy();
        innerMapper.addMixIn(targetClass, RevertDefaultDeserialize.class);
        Object o = innerMapper.convertValue(map, targetClass);
        // this will only work with simple json->bean property mapping
        ((Changes) o).changes(map.keySet());
        return o;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        Class<?> targetClass = ctxt.getContextualType().getRawClass();
        return new Deser(targetClass);
    }

    @JsonDeserialize
    interface RevertDefaultDeserialize {
    }
}

Here is how the bean from the question would look like. I would split up the JPA entity and the data transfer bean used in the controller interface, but here it's the same bean.

Changes could be supported by a base class if inheritance is possible, but here the interface itself is used directly.

@Data
class MyEntity implements Changes {
    private Integer id;
    private boolean aBoolean;
    private String aVeryBigString;

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private Set<String> changes;

    @Override
    public void changes(Set<String> changed) {
        this.changes = changed;
    }

    @Override
    public Set<String> changes() {
        return changes;
    }
}

and here is how it would be used

class HowToUseIt {
    public static void example(MyEntity bean) {
        if (bean.changed("id")) {
            Integer id = bean.getId();
            // ...
        }
        if (bean.changed("aBoolean")) {
            boolean aBoolean = bean.isABoolean();
            // ...
        }
        if (bean.changed("aVeryBigString")) {
            String aVeryBigString = bean.getAVeryBigString();
            // ...
        }
    }
}

Solution 14 - Java

If you will implement JpaRepository then you can use this.

@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);

Solution 15 - Java

There are a lot of other great approaches here, but I figured I would add mine since I haven't seen it mentioned and I think it has the added advantage that it can handle nullable fields without having to add a list of updated fields inline with the request. This approach has these properties:

  1. Only fields sent in the request are updated
  2. Missing fields are ignored
  3. Fields sent explicitly as null in the JSON are updated to null in the data store

So, given the following domain object:

public class User {
  Integer id;      
  String firstName;
  String lastName;
}

The controller method to incrementally update the user looks like the following, which could easily be extracted out to a static method suitable for any domain object using generics:

public class UserController {
  @Autowired ObjectMapper om;

  @Autowired
  @Qualifier("mvcValidator")
  private Validator validator;

  // assume this is a JPARepository
  @Autowired
  private UserRepository userRepo;

  @PostMapping(value = "/{userId}", consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Void> incrementalUpdate(@PathVariable("userId") Integer userId, 
    @RequestBody requestJson) {
    
    final User existingUser = this.userRepo.findById(userId).orElse(null);
    if(existingUser == null) { 
      return ResponseEntity.notFound().build();
    }

    // OPTIONAL - validate the request, since we can't use @Validated
    try {
      final User incomingUpdate = om.readValue(updateJson, User.class);
      final BeanPropertyBindingResult validationResult 
        = new BeanPropertyBindingResult(incomingUpdate, "user");
      this.validator.validate(incomingUpdate, validationResult);
      if (validationResult.hasErrors()) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
      }
    } catch (JsonProcessingException e) {
      return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }

    // merge the incoming update into the existing user
    try {
      this.om.readerForUpdating(existingUser).readValue(updateJson, User.class);
    } catch(IOException e) {
      return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }

    this.userRepo.save(existingUser);
    return ResponseEntity.noContent().build();
  }
}

Note that if your domain object has any nested objects or collections, they will need to be annotated with @JsonMerge, otherwise they will just be unconditionally overwritten by the incoming value instead of merged recursively.

Solution 16 - Java

My answer might be late but incase if there are people still facing the same problem. I have treid with PATCH with all the possible solutions but couldn't manage to parially update the object's fields. So i switched to POST and with post, i can update specific fields without changing the values of unchanged fields.

Solution 17 - Java

You may change boolean to Boolean and assign null value for all fields that you don't want to update. The only one not null value will define you which field client want to update.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmaelView Question on Stackoverflow
Solution 1 - JavavineView Answer on Stackoverflow
Solution 2 - JavaChexpirView Answer on Stackoverflow
Solution 3 - JavasnovelliView Answer on Stackoverflow
Solution 4 - JavaThorn GView Answer on Stackoverflow
Solution 5 - JavaknalliView Answer on Stackoverflow
Solution 6 - JavamanashView Answer on Stackoverflow
Solution 7 - JavaNoxView Answer on Stackoverflow
Solution 8 - JavakaytastropheView Answer on Stackoverflow
Solution 9 - JavaAxel GoetheView Answer on Stackoverflow
Solution 10 - JavaThe CoderView Answer on Stackoverflow
Solution 11 - JavaGrigory KislinView Answer on Stackoverflow
Solution 12 - JavaThomas NeebView Answer on Stackoverflow
Solution 13 - JavaPawel ZieminskiView Answer on Stackoverflow
Solution 14 - JavaOguzhan CevikView Answer on Stackoverflow
Solution 15 - JavaDominic A.View Answer on Stackoverflow
Solution 16 - JavaAssegdView Answer on Stackoverflow
Solution 17 - Javamvb13View Answer on Stackoverflow