Spring MVC PATCH method: partial updates
Asked Answered
L

17

67

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?

Libradalibrarian answered 25/7, 2013 at 14:14 Comment(1)
I've put together a post that describes an approach for using PATCH in Spring. And a working example is available on GitHub.Enchant
T
-20

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.

Tetrameter answered 25/7, 2013 at 14:33 Comment(8)
Seems like a good alternative, I upvoted, but what if someone uses the API and sends {"aVeryBigString":null} instead of {"aVeryBigString":""} to empty the string?Libradalibrarian
null value has to be used only to define that property is not used in update action. If you want to empty the string you have to pass only "". This rule has to be like a convention.Tetrameter
When jackson deserializes from the request body all missing fields will be null so you don't even have to explicitly set values to null.Rachal
@mvb13: Yeah, that could do the trick. I am going to wait a bit to see if someone has another answer. If not, I will accept your answer.Libradalibrarian
PATCH should be only used for sending only the properties to be updated .. according to https://mcmap.net/q/294586/-spring-mvc-patch-method-partial-updates and williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiotWeywadt
Downvoting because sending a PATCH with an attribute that has a null value is intrinsically different than sending a PATCH that does not include a value for an attribute (e.g., for an app adhering to the JSONAPI spec, I would expect the former to unset the attribute, and the later to leave the attribute unchanged).Outcaste
agree with @yaauie, you need to distinguish which fields are explicitly set by the client to null and which are just by default null when it comes to POJO model.Vivacity
While convention "use "", not null" may work for strings, it's not applicable for boxed primitive types (e.g. Integer), as they don't have such special empty valueAutoeroticism
V
25

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;
    }
}
Vivacity answered 8/2, 2016 at 3:57 Comment(5)
I like this answer. Do you have sample code for the mapper that accomplishes this in a generic fashion so that the same code could apply to each of the entity in the domain instead of having repeating code for each entity class. I assume it would need to use reflection to "sync" each of the property over from the HashMap to the domain model. I also wonder if this would have a performance impact?Shotputter
I just don't get it. How null-values in the Map are distinguishable from unexistent values? If Map implementation permits null-values, then result of map.get(unexistentKey) and map.get(nullValueKey) will be the same. If it doesn't permit null-values, then Jackson can't map json-null to this map. So, the Map isn't any more usable then Pojo to distinguish nulls from non-passed values.Engle
@djxak you need to have a convention that if client sends you an empty string then you clear the field - you will be able to detect that with a map. Alternatively, you could use Map.keySet to check which entries you have there (even those which have null values - meaning that the client asks to clear corresponding properties)Lippmann
@ruslan-stelmachenko, results of map.containsKey(unexistentKey) and map.containsKey(nullValueKey) would we differentAutoeroticism
Does anybody know how to perform javax annotation based validations? It it's DTO it's possible but that again one more problem for partial update.Marrano
H
11

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 }
]
Heilman answered 18/12, 2013 at 10:33 Comment(8)
This is the wrong patch. There's JSON Patch and HTTP Patch (which is a verb like get, post, put, etc). tools.ietf.org/html/rfc5789Discontent
@EricBrandel why do you say it's wrong? The example above uses both: PATCH HTTP method defined in RFC 5789, as well as json patch data format (application/json-patch+json) defined in RFC 6902. Moreover the HTTP PATCH method specification mentions [...] the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version. This implies using data format which explicitly defines operation, which application/json-patch+json doesMaurili
some reading on this in this blog post.Maurili
Llinking an article where the writer calls those who disagree with him "idiots" doesn't do a lot for me. Patching {"email": "[email protected]"} to update something isn't wrong. It is the most concise form of updating information on the server, and I would argue that it fully complies with RF5789. It is a fully encapsulated representation of the updates. JSON PATCH is overkill in the majority of cases, and doesn't address what the original poster was trying to do or alluding to.Discontent
See also #36908223Enclave
The OP is trying to use the JSON Merge Patch technique, which is a perfectly valid way of using HTTP PATCH as specified in RFC 7396 and acknowledged as a mea culpa in the stupid blog post people keep linking.Aurify
The JSO Merge Patch technique is good as long as you process the JSON directly and not a POJO created from the JSON. The reason is that with JSON, you have the ability to differentiate between a field set to a null value and a field left untouched. On the POJO side, both cases will be represented by a property with value null which is ambiguous. If you don't allow null values, you are fine. Otherwise, RFC6902 looks better to me.Halonna
An interesting direction to look at is using Java 8 Optional for fields. This way, if the field is not present in the JSON (meaning no update), the POJO field will be null. Otherwise, the field will contain an Optional instance that may wrap a null value. Some will also argue that Optionals should not be used for fields...Halonna
C
11

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();
    }

}
Cyrenaica answered 6/8, 2018 at 21:45 Comment(2)
What is associationLinks/Association? From where do I import it?Disrobe
@user import org.springframework.data.rest.webmvc.mapping.Associations;Cyrenaica
H
5

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.

Halonna answered 2/9, 2018 at 7:48 Comment(3)
Even though afaik Java's optionals are not intended to be used as fields, this still seems like the most straightforward solution to me and a perfect case for when it's very useful, even if not intentionally so.Dusa
@SebastiaanvandenBroek, I also like this approach. I created complete answer for another question where I used Optional together with MapStructBradney
I came independently to the same conclusion that, all things considered, this is the most elegant solution.Dactylogram
C
4

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.

Caroylncarp answered 25/7, 2013 at 15:13 Comment(2)
I know the point of PATCH. The JSON part is not the problem. The problem is the JSON deserialization. On the server side, I am receiving a Java object, not a JSON string (because of the magic of Spring MVC, and I would like to keep this magic). If I just received a JSON string, surely I could instantly know what the client has sent. With this simple JSON: { aBoolean: true }, I am receiving a full "MyEntity" object, with a null "aVeryBigString" property. The question is: how do I know if the "aVeryBigString" property was emptied by the client or just not sent?Libradalibrarian
Have a look at my comments on @Chexpis's answer. Using plain JSON together with PATCH method is against HTTP PATCH specification.Maurili
S
4

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.

Sero answered 1/10, 2013 at 8:7 Comment(0)
C
3

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.

Cupro answered 26/4, 2019 at 14:15 Comment(2)
You should not set directly the field without calling its setter method because the setter method could do some validation/conversion and directly setting the field value will bypass that security check.Disrobe
This might work in Kotlin where you can have validations within the property getters but it is bad practice for Java.Slumberous
S
3
@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 Custom Spring MVC HTTP Patch requests with Spring Data Rest functionality

Sensor answered 17/10, 2020 at 8:45 Comment(0)
E
1

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.

Eudemon answered 10/12, 2014 at 17:12 Comment(1)
This approach would not scale. If you want to support patch for just 1 entity, this is fine. If you have 100 entity classes in your codebase, you would end up with as many classes to do the path. So there has to be a better way.Shotputter
H
1

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.

Hypophosphate answered 5/9, 2017 at 7:32 Comment(0)
B
1

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
}
Bifilar answered 5/7, 2020 at 10:11 Comment(0)
K
1

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.

Kathlenekathlin answered 13/9, 2021 at 19:31 Comment(0)
C
0

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.

Cornejo answered 14/12, 2016 at 19:54 Comment(0)
A
0

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();
            // ...
        }
    }
}
Amp answered 24/12, 2020 at 0:13 Comment(0)
M
0

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);
Margetts answered 5/2, 2021 at 7:49 Comment(0)
D
-2

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.

Daydream answered 17/1, 2019 at 9:37 Comment(1)
Do not use post for general field updates. Use PUT or PATCH. Yes it can be used for an update, but because PUT is idempotent, it is best practice to use that to update an existing object so long as you have a reference for it.Cupro
T
-20

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.

Tetrameter answered 25/7, 2013 at 14:33 Comment(8)
Seems like a good alternative, I upvoted, but what if someone uses the API and sends {"aVeryBigString":null} instead of {"aVeryBigString":""} to empty the string?Libradalibrarian
null value has to be used only to define that property is not used in update action. If you want to empty the string you have to pass only "". This rule has to be like a convention.Tetrameter
When jackson deserializes from the request body all missing fields will be null so you don't even have to explicitly set values to null.Rachal
@mvb13: Yeah, that could do the trick. I am going to wait a bit to see if someone has another answer. If not, I will accept your answer.Libradalibrarian
PATCH should be only used for sending only the properties to be updated .. according to https://mcmap.net/q/294586/-spring-mvc-patch-method-partial-updates and williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiotWeywadt
Downvoting because sending a PATCH with an attribute that has a null value is intrinsically different than sending a PATCH that does not include a value for an attribute (e.g., for an app adhering to the JSONAPI spec, I would expect the former to unset the attribute, and the later to leave the attribute unchanged).Outcaste
agree with @yaauie, you need to distinguish which fields are explicitly set by the client to null and which are just by default null when it comes to POJO model.Vivacity
While convention "use "", not null" may work for strings, it's not applicable for boxed primitive types (e.g. Integer), as they don't have such special empty valueAutoeroticism

© 2022 - 2024 — McMap. All rights reserved.