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.
Comment From: wilkinsona
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.
Comment From: nosan
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:
```http request POST http://localhost:8080/people Content-Type: application/json Accept-Language: pt-BR
{ "name": "" }
HTTP Response:
```json
{
"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
Comment From: humbertoc-silva
I've only looked at the first branch, and it's hard to know exactly what you're looking at as the
MethodArgumentNotValidExceptionhas 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.messageorjakarta.validation.constraints.Size.message. If you add one or both of these tomessages.propertiesyou 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.
Comment From: humbertoc-silva
As I understood you want to interpolate and include validation errors in the
detailfield.I checked your first branch
spring-docand to achieve this you need to adjust a little bit yourmessages.propertiesmessages.properties
ini 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_BRlocale you also have to add the following file:messages_pt_BR.properties
ini 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:
```httpspec POST http://localhost:8080/people Content-Type: application/json Accept-Language: pt-BR
{ "name": "" } ```
HTTP Response:
json { "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.
Comment From: humbertoc-silva
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}).
Comment From: humbertoc-silva
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"
}
}
Comment From: nosan
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.propertieswhich do not overlap with Spring Framework. For exampleNotBlankPersonName. 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.propertiesinstead ofmessage.properties?
Comment From: philwebb
Thanks @nosan! It doesn't look like this is a Spring Boot bug so I'll close the issue.
Comment From: humbertoc-silva
@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.