3

I have this InstantDesrializer

@Slf4j
public class InstantDeserializer extends StdDeserializer<Instant> {

    public InstantDeserializer() {
        this(null);
    }

    public InstantDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Instant deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        log.info(node.asText());
        TemporalAccessor parse = null;
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Constants.DATE_TIME_FORMAT).withZone(ZoneOffset.UTC);
        try {
            parse = dateTimeFormatter.parse(node.asText());
        } catch (Exception e) {
            e.printStackTrace();
            throw new IOException();
        }
        log.info(Instant.from(parse).toString());
        return Instant.from(parse);
    }
}

And then corresponding IOException in @ControllerAdvice

@ExceptionHandler(IOException.class)
public ResponseEntity<String> handleIOException(IOException e) {
    return ResponseEntity.status(422).build();
}

And this in my DTO:

    @NotNull
    @JsonDeserialize(using = InstantDeserializer.class)
//    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    private Instant timestamp;

Even when uncommented @DateTimeFormat, it's not working

Ideally, it should return 422 status. But, it returns 400.

Maybe I'm just missing something so small, which I'm not able to figure out.

This approach was suggested here: Throw custom exception while deserializing the Date field using jackson in java

dkb
  • 4,389
  • 4
  • 36
  • 54
nirvair
  • 4,001
  • 10
  • 51
  • 85
  • First guess: You are getting the 400 because you are sending json as the body of a post or put, your web service framework (spring mvc?) tries to deserialize the json and fails. It would generate the 400, regardless of the original cause, because the expected input is non-valid, and this may happen before even routing the request to your code. Your handler may not get to see the exception. I wold suggest putting a breakpoint or logging in your handler to see if it is ever called. – joshp Mar 18 '19 at 03:23
  • So, I added the log in Deserializer and the log enters the catch statement. – nirvair Mar 18 '19 at 03:28
  • Did you log in the handler, does the handler ever get called? – joshp Mar 18 '19 at 03:51
  • Do you want the status to be show 422 instead of 400 in final response when Jackson deserialization fails? – dkb Mar 18 '19 at 06:47

3 Answers3

1

You do not need an handleIOException method, just add @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
to your CustomException.

import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class MyException extends JsonProcessingException {
    public MyException(String message) {
        super(message);
    }
}

So when you make invalid request with body

{"timestamp":"2018-04-2311:32:22","id":"132"}

Response will be:

{
    "timestamp": 1552990867074,
    "status": 422,
    "error": "Unprocessable Entity",
    "message": "JSON parse error: Instant field deserialization failed; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Instant field deserialization failed (through reference chain: TestDto[\"timestamp\"])"
}

Postman Response

With valid request works fine:

{"timestamp":"2018-04-23T11:32:22.213Z","id":"132"}

Response:

{
    "id": "132",
    "timestamp": {
        "nano": 213000000,
        "epochSecond": 1514700142
    }
}
dkb
  • 4,389
  • 4
  • 36
  • 54
0

Your Controller method is never called because JSON body parsing threw an exception.

  • Your @ContollerAdvice is not applied because the Controller method is not called.

  • Your handleIOException method is not called, and your 422 status is not applied.

I suspect this is the situation in more detail...

  1. An HTTP request includes a json body.

  2. The controller method matching the @RequestMapping and other annotations of the request takes an instance of your DTO Class as a parameter.

  3. Spring attempts to deserialize the incoming json body before calling your controlling method. It must do this in order to pass the DTO object.

  4. Deserialization uses your custom deserializer which throws an IOException.

  5. This IOException occurs before your controller method is called. In fact, your controller method is never called for this request.

  6. Spring handles the exception using its default behavior, returning an HTTP 400. Spring has a very broad RFC 7231 conception of HTTP 400.

  7. Since your controller method is never called, the @ControllerAdvice is never applied and your @ExceptionHandler does not see the exception. Status is not set to 422.

Why do I believe this?

I frequently see this kind of behavior from Spring, and I think it is the expected behavior. But I have not tracked down the documentation or read the source to be sure.

What can you do about it?

A simple approach which you may not like is to declare your controller method to take inputs that almost never fail, such as String.

  • You take over the responsibility for validating and deserializing inputs, and you decide what status and messages to return.

  • You call Jackson to deserialize. Your @ExceptionHandler methods are used.

  • Bonus: You can return the text of Jackson's often useful parse error messages. Those can help clients figure out why their json is rejected.

I would not be surprised if Spring offers a more stylish approach, a class to be subclassed, a special annotation. I have not pursued that.

What should you do about it?

400 vs. 422 is a case I prefer not to litigate. Depending on your priorities it might be best to accept Spring's convention.

RFC 7231 On Status 400

The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).

If the HTTP status code police pick on you, you could point to this and say "I perceive this input to be a client error." Then argue that 422 is inappropriate unless you are serving WebDAV just to keep them off balance.

joshp
  • 1,886
  • 2
  • 20
  • 28
0

The exception thrown by Jackson in case of a deserialization error is HttpMessageNotReadableException.

In your custom deserializer, you can throw your own deserialization exception which extends JsonProcessingException.

In your ControllerAdvice you can handle the HttpMessageNotReadableException and get the cause of this which is your custom exception. This way, you can throw the http code you want.

    @ExceptionHandler({HttpMessageNotReadableException.class})
    @ResponseBody
    public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
        Throwable cause = ex.getCause();
        if (cause.getCause() instanceof YourOwnException) {
            //Return your response entity with your custom HTTP code
        } 

        //Default exception handling
    }
Adnd
  • 1