Usually a request for a PUT / POST API contains some annotations like @NotBlank
, @NotNull
, @Valid
, etc
so that our request body gets validated automatically by Spring, for example.
In the context of a PATCH API:
- not provide the field means keep the field as is.
- provide the field as null means remove the field.
Using a normal json and checking in the class that represents our request if the field is null doesn't work as we don't know
if the field is null because the front-end explicitly provided it as null
(meaning remove the field) or if it's just because the field was not provided at all,
as all fields not provided will be automatically mapped as null in the class that represents our request.
Example: let's say the class that represents our request is called MyRequest
public record MyRequest(String name, LocalDate dateOfBirth) {
}
and the request body is something like
{
"name": "John Smith",
"dateOfBirth": null
}
or
{
"name": "John Smith"
}
Doing myRequest.dateOfBirth() == null
doesn't work. In both cases they will be null:
in the first case, it was explicitly provided as null
.
In the second case, it's null
just because the field was not provided.
That said, the solution is to use JsonMergePatch
as our request body, but there is a problem: we can't add (jakarta/javax) annotations to it,
so the request body won't be automatically validated by Spring
@Valid @RequestBody JsonMergePatch request
doesn't work.
This example works around this issue, showing how to make use of the (jakarta/javax) annotations to validate a Json Merge Patch so that the request body is correctly validated as if we were using a POST / PUT API.
Check:
http://localhost:8080/swagger-ui.html
We can see the swagger docs are generated correctly. This is possible due to the following annotation:
@Schema(implementation = PersonRequest.class) @RequestBody JsonMergePatch request
curl -X 'POST' \
'http://localhost:8080/people' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
}
}'
Response: 794b32b2-b187-47b3-9b65-62014acc332a
Check the result:
curl -X 'GET' 'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a'
Response:
{
"id": "794b32b2-b187-47b3-9b65-62014acc332a",
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
},
"address": null,
"contact": null
}
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"address": {
"address": "1428 Elm Street",
"city": "Los Angeles",
"postCode": "LA1234"
}
}
'
Response:
{
"id": "794b32b2-b187-47b3-9b65-62014acc332a",
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
},
"address": {
"address": "1428 Elm Street",
"city": "Los Angeles",
"postCode": "LA1234"
},
"contact": null
}
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"contact": {
"email": "[email protected]",
"phoneNumber": "12345678"
}
}
'
Response:
{
"id": "794b32b2-b187-47b3-9b65-62014acc332a",
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
},
"address": {
"address": "1428 Elm Street",
"city": "Los Angeles",
"postCode": "LA1234"
},
"contact": {
"email": "[email protected]",
"phoneNumber": "12345678"
}
}
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"contact": null
}
'
Response:
{
"id": "794b32b2-b187-47b3-9b65-62014acc332a",
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
},
"address": {
"address": "1428 Elm Street",
"city": "Los Angeles",
"postCode": "LA1234"
},
"contact": null
}
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"address": {
"city": "New York"
}
}
'
Response:
{
"id": "794b32b2-b187-47b3-9b65-62014acc332a",
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
},
"address": {
"address": "1428 Elm Street",
"city": "New York",
"postCode": "LA1234"
},
"contact": null
}
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"contact": {
"email": "invalid_email"
}
}
'
Response: 400 as it gets validated by @Email
annotation.
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"contact": {
"email": null
}
}'
Response:
{
"id": "794b32b2-b187-47b3-9b65-62014acc332a",
"personalDetails": {
"firstName": "Freddy",
"lastName": "Krueger",
"dateOfBirth": "1984-11-09"
},
"address": {
"address": "1428 Elm Street",
"city": "Los Angeles",
"postCode": "LA1234"
},
"contact": {
"email": null,
"phoneNumber": "12345678"
}
}
curl -X 'PATCH' \
'http://localhost:8080/people/794b32b2-b187-47b3-9b65-62014acc332a' \
-H 'accept: */*' \
-H 'Content-Type: application/merge-patch+json' \
-d '{
"personalDetails": {
"firstName": null
}
}'
Response: 400 as it gets validated by @NotBlank
annotation.