According to my knowledge:
PUT
- update object with its whole representation (replace)PATCH
- update object with given fields only (update)I'm using Spring to implement a pretty simple HTTP server. When a user wants to update his data he needs to make a HTTP PATCH
to some endpoint (let's say: api/user
). His request body is mapped to a DTO via @RequestBody
, which looks like this:
class PatchUserRequest {
@Email
@Length(min = 5, max = 50)
var email: String? = null
@Length(max = 100)
var name: String? = null
...
}
Then I use an object of this class to update (patch) the user object:
fun patchWithRequest(userRequest: PatchUserRequest) {
if (!userRequest.email.isNullOrEmpty()) {
email = userRequest.email!!
}
if (!userRequest.name.isNullOrEmpty()) {
name = userRequest.name
}
...
}
My doubt is: what if a client (web app for example) would like to clear a property? I would ignore such a change.
How can I know, if a user wanted to clear a property (he sent me null intentionally) or he just doesn't want to change it? It will be null in my object in both cases.
I can see two options here:
@Valid
right now.How should such cases should be properly handled, in harmony with REST and all good practices?
EDIT:
One could say that PATCH
shouldn't be used in such an example, and I should use PUT
to update my User. But what about model changes (e.g. adding a new property)? I would have to version my API (or the user endpoint alone) after every User change. E.g. I would have api/v1/user
endpoint which accepts PUT
with an old request body, and api/v2/user
endpoint which accepts PUT
with a new request body. I guess it's not the solution and PATCH
exists for a reason.
patchy is a tiny library I've come up with that takes care of the major boilerplate code needed to properly handle PATCH
in Spring i.e.:
class Request : PatchyRequest {
@get:NotBlank
val name:String? by { _changes }
override var _changes = mapOf<String,Any?>()
}
@RestController
class PatchingCtrl {
@RequestMapping("/", method = arrayOf(RequestMethod.PATCH))
fun update(@Valid request: Request){
request.applyChangesTo(entity)
}
}
Since PATCH
request represent changes to be applied to the resource we need to model it explicitly.
One way is to use a plain old Map<String,Any?>
where every key
submitted by a client would represent a change to the corresponding attribute of the resource:
@RequestMapping("/entity/{id}", method = arrayOf(RequestMethod.PATCH))
fun update(@RequestBody changes:Map<String,Any?>, @PathVariable id:Long) {
val entity = db.find<Entity>(id)
changes.forEach { entry ->
when(entry.key){
"firstName" -> entity.firstName = entry.value?.toString()
"lastName" -> entity.lastName = entry.value?.toString()
}
}
db.save(entity)
}
The above is very easy to follow however:
The above can be mitigated by introducing validation annotations on the domain layer objects. While this is very convenient in simple scenarios it tends to be impractical as soon as we introduce conditional validation depending on the state of the domain object or on the role of the principal performing a change. More importantly after the product lives for a while and new validation rules are introduced it's pretty common to still allow for an entity to be update in non user edit contexts. It seems to be more pragmatic to enforce invariants on the domain layer but keep the validation at the edges.
This is actually very easy to tackle and in 80% of cases the following would work:
fun Map<String,Any?>.applyTo(entity:Any) {
val entityEditor = BeanWrapperImpl(entity)
forEach { entry ->
if(entityEditor.isWritableProperty(entry.key)){
entityEditor.setPropertyValue(entry.key, entityEditor.convertForProperty(entry.value, entry.key))
}
}
}
Thanks to delegated properties in Kotlin it's very easy to build a wrapper around Map<String,Any?>
:
class NameChangeRequest(val changes: Map<String, Any?> = mapOf()) {
@get:NotBlank
val firstName: String? by changes
@get:NotBlank
val lastName: String? by changes
}
And using Validator
interface we can filter out errors related to attributes not present in the request like so:
fun filterOutFieldErrorsNotPresentInTheRequest(target:Any, attributesFromRequest: Map<String, Any?>?, source: Errors): BeanPropertyBindingResult {
val attributes = attributesFromRequest ?: emptyMap()
return BeanPropertyBindingResult(target, source.objectName).apply {
source.allErrors.forEach { e ->
if (e is FieldError) {
if (attributes.containsKey(e.field)) {
addError(e)
}
} else {
addError(e)
}
}
}
}
Obviously we can streamline the development with HandlerMethodArgumentResolver
which I did below.
I thought that it would make sense to wrap what've described above into a simple to use library - behold patchy. With patchy one can have a strongly typed request input model along with declarative validations. All you have to do is to import the configuration @Import(PatchyConfiguration::class)
and implement PatchyRequest
interface in your model.