I'm Getting Inadequate Error Messages from Spring Boot

Viewed 588

In my Spring Boot application, I specified my API using OpenApi 3.0.0. When I test its response to bad input, I'm not happy with some of the error messages. The messages are useful when Hibernate can't handle my input. They include the class, field, and even the illegal value. But when Spring Boot rejects my input without even entering my code, I just get the vague message The request cannot be fulfilled due to bad syntax. There's no information about what field is bad, or what object holds the bad field value.

When I specify my DTO in the .yaml file, two fields are required:

MenuItemOptionDto:
  type: object
  description: Option for a MenuItem
  properties:
    name:
      type: string
    deltaPrice:
      type: number
      description: Floating point price. Strings are easier to work with.
    id:
      type: integer
      format: int32
  required:
    - name
    - deltaPrice

But suppose I submit a DTO with a missing deltaPrice, like this: {"name": "onions"} The error message just says The request cannot be fulfilled due to bad syntax. I want the error message to say which DTO is incorrect, and which field is missing.

I have specified three relevant application properties. Any one of these will give me Hibernate validation error messages, but none give me spring-boot validation messages:

server.error.include-message=always
server.error.include-binding-errors=always
server.error.include-exception=true

And I've received advise to add a validator bean to my main application, which didn't help:

@ComponentScan(basePackages = {"com.myWork.dummy","org.openapitools",})
@EnableCaching
@SpringBootApplication
public class ServerMaster implements CommandLineRunner {
  private static final Logger log = LoggerFactory.getLogger(ServerMaster.class);
  public static void main(String[] args) {
    new SpringApplication(ServerMaster.class).run(args);
  }

  @Override
  public void run(String... arg0) { ... }

  // This was suggested at https://stackoverflow.com/questions/49538896/spring-boot-error-message-doesnt-work
  // in order to give me better error messages when OpenAPI validations are triggered, but it doesn't help.
  @Bean public Validator validator() {
    return new LocalValidatorFactoryBean();
  }
}

When I generate the code, it doesn't matter if I turn on the performBeanValidation or useBeanValidation options. The generated code doesn't change. Either way, the @NotNull annotations are applied to the getters for the name and deltaPrice fields, and these are getting honored by the server, but without useful error messages.

Finally, I'm using Spring-Boot 2.3.4, and I declare a dependency on Spring Boot annotations:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Spring-Boot correctly rejects the input because the OpenAPI generator puts @NotNull annotations on the getters of the generated MenuItemOptionDTO, but since the code is generated, I can't customize them with an error message, and I don't want to turn off the generator. How can I get Spring or OpenAPI to give me better error messages?

Test Case

To see these messages in action, check out the code at https://github.com/SwingGuy1024/SpringBootDemo.22.05.25

2 Answers

The default SpringBoot error-handler does not provide a response body for MethodArgumentNotValidException:

    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return this.handleExceptionInternal(ex, (Object)null, headers, status, request);
    }

The good news: you can override this in your GlobalResponseExceptionHandler class:

  @Override
  protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    return handleExceptionInternal(ex, ex.getBindingResult(), headers, status, request);
  }

In the above code, we simply return the entire binding-result as the response body. If you want to, you can tweak this (e.g. only include the errors).

When you call the controller with the invalid payload, you'll now get the following response:

{
  "timestamp": "2022-05-28T19:40:47.295+00:00",
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "message": "Validation failed for object='menuItemOptionDto'. Error count: 1",
  "errors": [
    {
      "codes": [
        "NotNull.menuItemOptionDto.deltaPrice",
        "NotNull.deltaPrice",
        "NotNull.java.math.BigDecimal",
        "NotNull"
      ],
      "arguments": [
        {
          "codes": [
            "menuItemOptionDto.deltaPrice",
            "deltaPrice"
          ],
          "arguments": null,
          "defaultMessage": "deltaPrice",
          "code": "deltaPrice"
        }
      ],
      "defaultMessage": "must not be null",
      "objectName": "menuItemOptionDto",
      "field": "deltaPrice",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
    }
  ],
  "path": "/demo/admin/menuItem/addOption/1"
}

Based on OpenAPI generator to spring-boot with custom java validations

  • You can add some another validation layer in your code, which is independent of OpenAPI generator. This layer will be called from
    PetsController and PetsController will validate only basic OpenAPI
    known constraints.

  • You can add you validations not via annotations, but via xml config as shown here.

  • maybe something else.

  • Hack it a bit. I was looking for a solution in which my custom validation will be defined in OpenAPI spec same way as “required”.
    Naturally I decided not to use solutions 1 or 2 (even thought it
    might be the right way for a lot of cases). I found out the
    openapi-generator actually provides a way of modifying the way the
    code is generated. That means that I can actually define custom
    constraint in OpenAPI specs as my own made up properties.

Please follow instructions in the above link for implementing last method.

Related