In our frontend, we use Axios, which by default serializes array parameters like this:
arr[]=HELLO&arr[]=WORLD
However, this caused 400 Bad Request errors on our Spring Boot (Tomcat) backend. To fix that, we added the following configuration:
server:
tomcat:
relaxed-query-chars: ['[', ']', '{', '}']
After applying this setting, everything appeared to work fine — until we encountered a strange case.
We have the following request DTO:
@Getter
@AllArgsConstructor
@ToString
@ParameterObject
public class ExampleRequestDto {
@Parameter(description = "example")
private List<SomeEnum> arr;
}
Our frontend sends a request like:
GET /api/example?arr[]=HELLO&arr[]=WORLD
ExampleRequestDto(arr=[HELLO, WORLD])
ExampleRequestDto(arr=[HELLO])
Only the first value is present; the rest are silently ignored. No exceptions or warnings were thrown.
We changed the frontend to send:
GET /api/example?arr=HELLO&arr=WORLD
This works perfectly even without @Setter on the DTO.
Then, we added @Setter to the DTO, and the original arr[]=... format also worked correctly — all values were bound.
arr[]=HELLO&arr[]=WORLD require @Setter to bind correctly, but arr=HELLO&arr=WORLD does not?@ModelAttribute binding?I suspect this is due to Spring’s reflection mechanism, but I’m not sure.
Any official documentation, related GitHub issues, or explanations would be highly appreciated.
The difference comes from internal implementation of Spring's ServletRequestDataBinder
There are two steps relevant for creating the object:
ServletRequestDataBinder.construct() - This method scans the constructors of the DTO and creates the object. If the available constructor has arguments, it uses request.getParameterValues(...) to find appropriate values. Note that request.getParameterValues("arr") returns null if request parameter is arr[]
ServletRequestDataBinder.bind() - This method parses request parameters, normalizes them ("arr[]" becomes "arr"), then scans for setters and applies the values using setters. In this phase it doesn't matter if request param is called "arr[]" or "arr". But it is only applicable if setters exist
Note: if no-args constructor exists, the behavior will be the same because 1st phase will construct object with null values and only setters become relevant.
To answer your questions:
1.Why does
arr[]=HELLO&arr[]=WORLDrequire@Setterto bind correctly, but arr=HELLO&arr=WORLD does not?
arr=HELLO&arr=WORLD can be handled by constructor
arr[]=HELLO&arr[]=WORLD can't be handled by constructor, and is handled by attribute processing. During attribute processing the request parameters are normalized so it doesn't matter if they are called "arr[]" or "arr"
2.Is there a difference in how Spring interprets these two formats during
@ModelAttributebinding?
No. This is handled by bind attribute which normalizes array parameters so it doesn't matter if they are called "arr[]" or "arr"
3.Is this behavior caused by Spring MVC’s internal binder or by Tomcat’s parameter parser?
Spring internal binder
4.Why is the issue silent (i.e., no exception or binding error), even when values are missing?
By default, all DTO attributes are considered optional. Values are only populated for accessible fields using values received in request. If no appropriate value is found, value will be null. To change this behavior, you can use Bean Validations to specify which attributes are required to have a value when reaching controller.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With