javajsonspringjackson

Spring Boot controller to handle invalid JSON


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?


Solution

  • 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"
    }