I have a Spring Boot controller that consumes JSON:
@PostMapping(
value = "/shipdetails")
public ResponseEntity acceptShip(@RequestBody Ship ship, HttpServletRequest request) {
shipService.checkShip(ship);
return new ResponseEntity<>(HttpStatus.OK);
}
There is a corresponding Ship Entity:
public class Ship implements Serializable {
@JsonProperty("Name")
private String name;
@JsonProperty("Owner")
private String owner;
public Ship(String name, String owner) {
this.name = name;
this.owner = owner; }
public Ship() {
}
// Getters and Setters removed for brevity
And finally the service:
@Service
public class ShipService {
Boolean checkShip(Ship ship) {
if (ship.getName().equals("Queen Mary")) {
// do something
}
//edited for brevity
Sample invalid JSON:
{
"name_wrong":"test name",
"owner":"Lloyds Shipping"
}
Currently the error I get on the stacktrace if I send invalid JSON is (service layer) : Cannot invoke "String.equals(Object)" because the return value of "com.ships.Ship.getName()" is null
.
Jackson needs the no-args constructor for de-serialization.
When inspecting in the debugger the Ship entity has a owner set, but not a name set, so its not the the whole entity is null - just one field.
I tried my Ship entity without a default no args constructor so you could not even pass entity with a null field to the service but then it fails with even valid JSON.
How and where should the exception for invalid JSON be handled?
Need to add few annotations and checks for the validation of the request body json.
1. Add dependency in pom.xml for the below annotations-
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.1.1</version>
</dependency>
2. @Valid with Request Body
@PostMapping(value = "/shipdetails")
public ResponseEntity acceptShip(@Valid @RequestBody Ship ship,
HttpServletRequest request)
3. @NotNull with the mandatory attributes
public class Ship implements Serializable {
@NotNull(message = "The name is mandatory")
@JsonProperty("Name")
private String name;
@JsonProperty("Owner")
private String owner;
4. Additional null check before equals method
Boolean checkShip(Ship ship) {
if (StringUtils.isNotBlank(ship.getName())
&& ship.getName().equals("Queen Mary")) {
// do something
}
Output with Invalid json:
Input
{
"name_wrong":"test name",
"owner":"Lloyds Shipping"
}
Output: You will get a 400 Bad request status with the long error stack trace includes something like this
"message": "Validation failed for object='ship'. Error count: 1",
"errors": [
{
"codes": [
"NotNull.ship.name",
"NotNull.name",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"ship.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "The name is mandatory",
"objectName": "ship",
"field": "name",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
]
Note:
Some Additional code for handling the MethodArgumentNotValidException
exception using our custom exceptionHandler so that the validation will be in a proper formatted response instead of lengthly error stacktrace and control the status code as well as shown below:
@ControllerAdvice
public class ValidationExceptionHandler {
/**
* For an invalid input, spring framework will throw an
MethodArgumentNotValidException exception
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> notValidInput(MethodArgumentNotValidException e) {
Map<String,String> errorMap = e.getAllErrors()
.stream()
.collect(Collectors.toMap(x -> ((FieldError)x).getField(),
b -> b.getDefaultMessage(),(p,q) -> p, LinkedHashMap::new));
return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}
}
Formatted output for invalid json:
{
"name": "The name is mandatory"
}