'Spring @Valid Message Not Coming Through in Response

I have a CQRS Rest API using Spring and Axon. Validation is setup for inputs using the javax.validation library. The validation is working properly, and catching that the 'username' needs to be at least 2 character long. However the message associated with the failed validation is not showing up in the response to the Post request.

The response is ultimately missing 'errors' and 'message' info and I am not sure why. Doesn't MethodArgumentNotValidException get pushed to the try-catch block and get handled? If the validation is working properly, why isn't the message going out with the '400' status response?

What am I missing?

This is the response I am trying to get:

{
    "timestamp": "2022-04-24T01:23:27.809+00:00",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Size.registerUserCommand.user.account.username",
                "Size.user.account.username",
                "Size.username",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
               {
                    "codes": [
                        "registerUserCommand.user.account.username",
                        "user.account.username"
                    ],
                    "arguments": null,
                    "defaultMessage": "user.account.username",
                    "code": "user.account.username"
               },
               2134838498,
               2
           ],
           "defaultMessage": "username must have a minimum of 2 characters",
           "objectName": "registerUserCommand",
           "field": "user.account.username",
           "rejectedValue": "m",
           "bindingFailure": false,
           "code": "Size
        }
    ],
    "message": "Validation failed for object='registerUserCommand'. Error count: 1",
    "path": "/api/v1/registerUser"
}

This is the response I actually get:

{
    "timestamp": "2022-04-24T01:23:27.809+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "",
    "path": "/api/v1/registerUser"
}

Post request that is being sent to the controller:

{
    "user": {
        "firstname": "Mike",
        "lastname": "Jacobs",
        "emailAddress": "[email protected]",
        "account": {
            "username": "m",
            "password": "mik959593e0",
            "roles": [
                "READ_PRIVILEGE"
            ]
        }
    }
}

Console output from Post request:

2022-04-24 20:32:21.339  INFO 11040 --- [nio-8081-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-04-24 20:32:21.340  INFO 11040 --- [nio-8081-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-04-24 20:32:21.343  INFO 11040 --- [nio-8081-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms
2022-04-24 20:32:22.120  WARN 11040 --- [nio-8081-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.springbank.user.cmd.api.dto.RegisterUserResponse> com.springbank.user.cmd.api.controllers.RegisterUserController.registerUser(com.springbank.user.cmd.api.commands.RegisterUserCommand): [Field error in object 'registerUserCommand' on field 'user.account.username': rejected value [m]; codes [Size.registerUserCommand.user.account.username,Size.user.account.username,Size.username,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [registerUserCommand.user.account.username,user.account.username]; arguments []; default message [user.account.username],2147483647,2]; default message [username must have a min of 2 characters]] ]

The Controller that accepts the request:

    package com.springbank.user.cmd.api.controllers;

    import com.springbank.user.cmd.api.commands.RegisterUserCommand;
    import com.springbank.user.cmd.api.dto.RegisterUserResponse;
    import org.axonframework.commandhandling.gateway.CommandGateway;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import javax.validation.Valid;
    import java.util.UUID;
    
    @RestController
    @RequestMapping(path = "/api/v1/registerUser")
    public class RegisterUserController {
         private final CommandGateway commandGateway;

         @Autowired
         public RegisterUserController(CommandGateway commandGateway) {
             this.commandGateway = commandGateway;
         }
    
         @PostMapping
         public ResponseEntity<RegisterUserResponse> registerUser(@Valid @RequestBody RegisterUserCommand command) {
             var id = UUID.randomUUID().toString();
             command.setId(id);
    
             try{
                commandGateway.send(command);
                return new ResponseEntity<>(new RegisterUserResponse( id, "User Successfully Registered"), HttpStatus.CREATED);
    
             }catch (Exception e) {
                 var safeErrorMessage = "Error while processing register user request for id - " + id;
                 System.out.println(e.toString());
    
                 return new ResponseEntity<>(new RegisterUserResponse( id, safeErrorMessage), HttpStatus.INTERNAL_SERVER_ERROR);
             }
         }
    }

The Command:

package com.springbank.user.cmd.api.commands;

import com.springbank.user.core.models.User;
import lombok.Builder;
import lombok.Data;
import org.axonframework.modelling.command.TargetAggregateIdentifier;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@Data
@Builder
public class RegisterUserCommand {

    @TargetAggregateIdentifier
    private String id;
    @NotNull(message = "Need to supply User info")
    @Valid
    private User user;
}

The User model:

package com.springbank.user.core.models;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Document(collection = "users")
public class User {
    @Id
    private String id;
    @NotEmpty(message = "firstname is mandatory")
    private String firstname;
    @NotEmpty(message = "lastname is mandatory")
    private String lastname;
    @Email(message = "please provide a valid email")
    private String emailAddress;
    @NotNull(message = "Please provide account Credentials")
    @Valid
    private Account account;
}

Account model where the validation is happening:

package com.springbank.user.core.models;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Account {
    @Size(min = 2, message = "username must have a min of 2 characters")
    private String username;
    @Size(min = 7, message = "password must have a min of 7 characters")
    private String password;
    @NotNull(message = "specify at least one user role")
    private List<Role> roles;
}

The RegisterUserResponse:

package com.springbank.user.cmd.api.dto;


public class RegisterUserResponse extends BaseResponse{
    private String id;

    public RegisterUserResponse(String id, String message) {
        super(message);
        this.id = id;
    }
}

    

The BasicResponse:

package com.springbank.user.cmd.api.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class BaseResponse {
    private String Message;
}


Solution 1:[1]

Since you annotate the controller method argument with @Valid RegisterUserCommand, the validation of the object will take place before the execution enters your try/catch block.

If you look at the below method located in ResponseEntityExceptionHandler then you'll see that the default implementation looks like this. As you can see, the default is to send null as body, which causes the details to be left out.

protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {

    return handleExceptionInternal(ex, null, headers, status, request);
}

Hence, if you want to handle that error differently you would need to add a customized exception handling for that exception type and override the default handler.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1