Skip to content

Problems with Spring and Bean Validation interpolation messages #42773

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
humbertoc-silva opened this issue Oct 17, 2024 · 9 comments
Closed
Labels
status: invalid An issue that we don't feel is valid

Comments

@humbertoc-silva
Copy link

Hi,

I am using:

  • Spring Boot: 3.2.10
  • Spring Framework: 6.1.13
  • Hibernate Validator: 8.0.1.Final

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 17, 2024
@wilkinsona
Copy link
Member

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.

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Oct 17, 2024
@nosan
Copy link
Contributor

nosan commented Oct 17, 2024

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

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Oct 17, 2024
@humbertoc-silva
Copy link
Author

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
Copy link
Author

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
Copy link
Author

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
Copy link
Author

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
Copy link
Contributor

nosan commented Oct 17, 2024

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:

  • Use different codes in your message.properties which do not overlap with Spring Framework. For example NotBlankPersonName. You will be able to use {min}, {max}, ${validatedValue}, etc. and will have fully interpolated message provided by Spring Boot and Hibernate.
  • Don't use ResponseEntityExceptionHandler. Same as first option, but don't need to think about code overlaps.
  • Use Spring Framework Message Interpolation {0}, {1} etc. but in that case, {min}, {max}, etc. placeholders will not be possible to use.
  • Use ValidationMessages.properties instead of message.properties?

@philwebb
Copy link
Member

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

@philwebb philwebb closed this as not planned Won't fix, can't repro, duplicate, stale Oct 18, 2024
@philwebb philwebb added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels Oct 18, 2024
@humbertoc-silva
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

5 participants