Description
Note: I also raised this topic as a SO question: https://stackoverflow.com/q/77645476/827950
When attempting to upgrade our Spring Boot + Spring Web based REST API from Spring Boot 3.1.6 to 3.2.0, we encountered an unexpected change of behavior in databinding of query parameters. In some cases it leads to errors, in others it might cause tricky bugs.
The following example shows our situation: a REST controller with a simple GET endpoint that retrieves its parameters as an object that contains a nested object:
@RestController
@RequestMapping("/builder")
public class DemoLombokDataBuilder {
@Data
@Builder
static class Dto {
String field;
Nested nested;
}
@Data
static class Nested {
String nestedField;
}
@GetMapping
public String demo(Dto dto) {
String result = dto.getField();
if (dto.getNested() != null)
result += dto.getNested().getNestedField();
else
result += ", nested is empty";
return result;
}
With Spring Boot 3.1.6, the result of calling the endpoint with or without the nested field are:
URL | Result |
---|---|
http://localhost:8080/builder?field=foo | foo, nested is empty |
http://localhost:8080/builder?field=foo&nested.nestedField=bar | foobar |
With Spring Boot 3.2.0 however, the results are:
URL | Result |
---|---|
http://localhost:8080/builder?field=foo | foonull |
http://localhost:8080/builder?field=foo&nested.nestedField=bar | foobar |
So instead of not instantiating the nested object at all when its field is not provided, Spring Boot 3.2 instantiates the nested object with empty values. This can lead to bugs - and it's even worse when validation is used and the nested object has mandatory fields annotated with @NotNull
. In that case Spring Boot 3.2 throws HTTP 400, while the requests worked fine in 3.1.
To us, this seems like a change between minor version that can cause existing code to break, and therefore a bug. Is this expected behavior or did we miss something from the docs or upgrade guides?
Additional Details
In my further tests, the issue only occurs when the DTO class can only be constructed via an all-args-constructor.
With this DTO:
@Data
static class Dto {
String field;
Nested nested;
}
both 3.1 and 3.2 have the same results:
URL | Result |
---|---|
http://localhost:8080/builder?field=foo | foo, nested is empty |
http://localhost:8080/builder?field=foo&nested.nestedField=bar | foobar |
I pushed the full examples to this Github repo: https://github.com/flpa/springboot-databinder-change-tests