spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.95k stars 40.65k forks source link

Problems with Spring and Bean Validation interpolation messages #42773

Closed humbertoc-silva closed 3 days ago

humbertoc-silva commented 4 days ago

Hi,

I am using:

I am using the default Spring Boot auto-configuration, there is no customization on the project. I have a Controller Advice that is inherent from org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler, and I want to use Problem Details as a response error format in my project.

1 - I need to customize my validation messages and interpolate some values. I am trying to follow the Spring Framework documentation. However following the documentation instructions I got the default messages from Bean Validation (in my language pt-BR).

2 - Then I tried to put the Spring codes on the annotations message attribute. I got the messages from the messages.properties file but the arguments {0}, {1}, {2}, etc. do not were interpolated by Spring.

3 - Finally, I changed the strategy and resolved to use the Bean Validation interpolation format, I got the correct messages, but when Spring Boot tried to resolve the Problem Detail fields I got an unexpected exception.

The following project can be used to simulate the problems: spring-boot-bean-validation-message-interpolation-issue Public

Three branches simulate the respective problems:

1 - spring-doc 2 - spring-doc-with-message 3 - bean-validation

I read the Spring documentation many times and debugged the project, but I did not find a way to make the project work as expected.

Let me know if I missed some steps to make Bean Validation work with Spring Boot and be able to customize my messages according to the official documentation.

wilkinsona commented 4 days ago

I've only looked at the first branch, and it's hard to know exactly what you're looking at as the MethodArgumentNotValidException has quite a bit of state, but there seems to be a misunderstanding about the default message.

The default message in the object errors is resolved by looking up jakarta.validation.constraints.NotBlank.message or jakarta.validation.constraints.Size.message. If you add one or both of these to messages.properties you should see that the default message in the error changes accordingly.

I'm not going to investigate further at this point as I suspect the second and third problems may be a knock-on effect of the misunderstanding that's caused the first. If applying the change suggested above does not help with the second and third problems and you would like us to investigate further, please update them so that there's a test that we can run that precisely reproduces the problem rather than us trying to guess what part of the state in the debugger it is that you consider to be incorrect.

nosan commented 4 days ago

As I understood you want to interpolate and include validation errors in the detail field.

I checked your first branch spring-doc and to achieve this you need to adjust a little bit your messages.properties

messages.properties

NotBlank.person.name=The field {0} must not be blank
Size.person.name=The size of the {0} field must be between {2} and {1}
problemDetail.org.springframework.web.bind.MethodArgumentNotValidException={0}{1}

If you would like to support pt_BR locale you also have to add the following file:

messages_pt_BR.properties

NotBlank.person.name=O campo {0} n\u00e3o deve estar em branco
Size.person.name=O tamanho do campo {0} deve estar entre {2} e {1}

HTTP Request:

POST http://localhost:8080/people
Content-Type: application/json
Accept-Language: pt-BR

{
  "name": ""
}

HTTP Response:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "O campo name não deve estar em branco, and O tamanho do campo name deve estar entre 1 e 50",
  "instance": "/people"
}

https://docs.spring.io/spring-framework/reference/6.1-SNAPSHOT/web/webmvc/mvc-ann-rest-exceptions.html#mvc-ann-rest-exceptions-render

humbertoc-silva commented 4 days ago

I've only looked at the first branch, and it's hard to know exactly what you're looking at as the MethodArgumentNotValidException has quite a bit of state, but there seems to be a misunderstanding about the default message.

The default message in the object errors is resolved by looking up jakarta.validation.constraints.NotBlank.message or jakarta.validation.constraints.Size.message. If you add one or both of these to messages.properties you should see that the default message in the error changes accordingly.

I'm not going to investigate further at this point as I suspect the second and third problems may be a knock-on effect of the misunderstanding that's caused the first. If applying the change suggested above does not help with the second and third problems and you would like us to investigate further, please update them so that there's a test that we can run that precisely reproduces the problem rather than us trying to guess what part of the state in the debugger it is that you consider to be incorrect.

Hi @wilkinsona, thank you for the reply. The main problem that I tried to show in the spring-doc branch was that maybe the Spring documentation was incomplete. I know that Bean Validation has this default message code and if I put them in my messages.properties the message will work. But I am trying to do the things as the Spring documentation explains, using the documentation example:

record Person(@Size(min = 1, max = 10) String name) {
}

@Validated
public class MyService {

    void addStudent(@Valid Person person, @Max(2) int degrees) {
        // ...
    }
}

The example does not use the message property and I tried to do that same way, so I got the default Bean Validation message.

On the branch spring-doc-with-message I put the Spring code on the message attribute, I got the message but Spring did not interpolate the messages.

And on the bean-validation it was worst, using Bean Validation interpolation way Spring Boot broke with an exception.

humbertoc-silva commented 4 days ago

As I understood you want to interpolate and include validation errors in the detail field.

I checked your first branch spring-doc and to achieve this you need to adjust a little bit your messages.properties

messages.properties

NotBlank.person.name=The field {0} must not be blank
Size.person.name=The size of the {0} field must be between {2} and {1}
problemDetail.org.springframework.web.bind.MethodArgumentNotValidException={0}{1}

If you would like to support pt_BR locale you also have to add the following file:

messages_pt_BR.properties

NotBlank.person.name=O campo {0} n\u00e3o deve estar em branco
Size.person.name=O tamanho do campo {0} deve estar entre {2} e {1}

HTTP Request:

POST http://localhost:8080/people
Content-Type: application/json
Accept-Language: pt-BR

{
  "name": ""
}

HTTP Response:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "O campo name não deve estar em branco, and O tamanho do campo name deve estar entre 1 e 50",
  "instance": "/people"
}

https://docs.spring.io/spring-framework/reference/6.1-SNAPSHOT/web/webmvc/mvc-ann-rest-exceptions.html#mvc-ann-rest-exceptions-render

Hi @nosan, thank you for the reply.

Yes, if I try to use the detail message it will work, but this occurs because Spring finishes the interpolation after validation using the method org.springframework.web.ErrorResponse#updateAndGetBody, but if you see the individual field messages they will be incomplete, without interpolation and this is the problem that I showed on the second branch, spring-doc-with-message.

humbertoc-silva commented 4 days ago

I will update the branch spring-doc-with-message to return the messages without interpolation, this way will be easier to see my point.

I need to customize individual validation messages with Spring way (using placeholders like {0}...) or Bean Validation way (using expressions and parameters values like {min}, {max}).

humbertoc-silva commented 4 days ago

I have just updated the branch spring-doc-with-message, now it is possible to see that the validation messages were not interpolated appropriately.

Result:

{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "detail": "Invalid request content.",
    "instance": "/people",
    "errors": {
        "Size": "The size of the {0} field must be between {1} and {2}",
        "NotBlank": "The field {0} must not be blank"
    }
}
nosan commented 3 days ago

Some time ago, Spring Boot introduced Bean Validation Message Interpolation via MessageSource (see: PR #17530).

The primary goal of this enhancement was to utilize MessageSource to replace any placeholders, and then, delegate the final interpolation to Hibernate's Bean Validation.

Let’s consider the following example:

message.properties

NotBlank.person.name=The field name must not be blank
Size.person.name=The size of the name field must be between {min} and {max}

Additionally, if you remove the ExceptionHandlerController and add server.error.include-binding-errors=always to your application.properties file, and then make an HTTP request, you will get the following result:

{
  "timestamp": "2024-10-17T20:13:29.504+00:00",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
        "Size.person.name",
        "Size.name",
        "Size.java.lang.String",
        "Size"
      ],
      "arguments": [
        {
          "codes": [
            "person.name",
            "name"
          ],
          "arguments": null,
          "defaultMessage": "name",
          "code": "name"
        },
        50,
        1
      ],
      "defaultMessage": "The size of the name field must be between 1 and 50",
      "objectName": "person",
      "field": "name",
      "rejectedValue": "",
      "bindingFailure": false,
      "code": "Size"
    },
    {
      "codes": [
        "NotBlank.person.name",
        "NotBlank.name",
        "NotBlank.java.lang.String",
        "NotBlank"
      ],
      "arguments": [
        {
          "codes": [
            "person.name",
            "name"
          ],
          "arguments": null,
          "defaultMessage": "name",
          "code": "name"
        }
      ],
      "defaultMessage": "The field name must not be blank",
      "objectName": "person",
      "field": "name",
      "rejectedValue": "",
      "bindingFailure": false,
      "code": "NotBlank"
    }
  ],
  "path": "/people"
}

As you can see, the interpolation works as expected.

However, when you have added ExceptionHandlerController extending ResponseEntityExceptionHandler, things changed significantly.

The main issue is that the Spring Framework also attempts to resolve Bean Validation's codes using MessageSource. For the Person.name field that is being validated, it will attempt to resolve the following codes:

[Size.person.name, Size.name, Size.java.lang.String, Size]
[person.name, name]
[NotBlank.person.name, NotBlank.name, NotBlank.java.lang.String, NotBlank]

As you can see, the code NotBlank.person.name is present in message.properties with {min} and {max} placeholders. Since the Spring Framework does not know what {min} and {max} represent, this leads to the following exception:

Failure in @ExceptionHandler com.example.demo.ExceptionHandlerController#handleException(Exception, WebRequest)
java.lang.IllegalArgumentException: can't parse argument number: min

With that in mind, I can suggest the following options:

philwebb commented 3 days ago

Thanks @nosan! It doesn't look like this is a Spring Boot bug so I'll close the issue.

humbertoc-silva commented 8 hours ago

@nosan and @wilkinsona I took some time to investigate how things work deeply and understood exactly how Spring works with Bean Validation. It was a misunderstanding on my side believing that Spring would interpolate the messages automatically. I saw that I needed to use one of the MessageSource#getMessage on my own to get the interpolated message. Now I can choose between Bean Validation way or Spring way interpolation without any errors, and if I decide to go with Spring way I know that I need to use some getMessage method.

Thank you.