Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
Version
Jackson 3.x (tools.jackson.databind), used via Spring WebMvc (Spring Boot 4 RC / Spring Framework 7).
With Jackson 2.x (com.fasterxml.jackson.databind) the behavior is different (see "Expected vs Actual" section).
Description
After upgrading from Jackson 2.x to Jackson 3.x (package tools.jackson), I noticed a behavior change in constructor selection when deserializing a simple POJO that has:
- a no-args constructor
- an all-args constructor
- a field with a default value (
= true)
In Jackson 2.x, deserialization of a JSON request body into this POJO would use the no-args constructor and keep the field default (true) when the property is not present in the JSON.
In Jackson 3.x, for the same code and same JSON, deserialization now appears to prefer the all-args constructor. As a result, the field is set to null instead of keeping the default value.
This is observable in a Spring MVC controller receiving the object via @RequestBody.
Generated by ChatGPT
Version Information
tools.jackson.core:jackson-databind - 3.0.1
Reproduction
Minimal reproducible example
POJO
In my real project this is generated with Lombok, but the essence is:
// Conceptual shape (Lombok-generated no-args + all-args + builder)
public class UserDto {
private Boolean enabled = true; // default value
public UserDto() {
// default constructor
// field initializer sets enabled = true
}
public UserDto(Boolean enabled /*, other fields... */) {
this.enabled = enabled;
// other assignments...
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
// equals/hashCode/toString omitted
}
In reality I’m using Lombok annotations similar to:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDto {
@Builder.Default
private Boolean enabled = true;
}
but the important part is: both a no-args and an all-args constructor exist, and the field has a default value.
Controller usage (Spring WebMvc)
@PostMapping("/users")
public UserDto addUser(@RequestBody UserDto userDto) {
// Breakpoint here
return userDto;
}
JSON sent to the endpoint
{
"username": "foo"
// no "enabled" field at all
}
Steps to reproduce
- Use Jackson 2.x (
com.fasterxml.jackson.databind) with theUserDtoclass above. - POST the JSON shown above to the controller that takes
@RequestBody UserDto. - Observe
userDto.getEnabled()at the start of the controller method. - Upgrade to Jackson 3.x (
tools.jackson.databind) keeping the same code and JSON. - Repeat the request and observe
userDto.getEnabled()again.
Expected behavior (Jackson 2.x)
With Jackson 2.x and this POJO:
- Jackson appears to use the no-args constructor and then setters / field access.
- Since the JSON omits
enabled, the field keeps its default value from the field initializer.
Result at the controller entry point:
userDto.getEnabled() == true
Actual behavior (Jackson 3.x)
With Jackson 3.x (same code and JSON):
- Jackson now appears to choose the all-args constructor for deserialization.
- Because the JSON does not contain the
enabledproperty, theenabledparameter of the all-args constructor isnull. - The constructor assigns
this.enabled = enabled, overwriting the field default.
Result at the controller entry point:
userDto.getEnabled() == null
Switching back to Jackson 2.x restores the old behavior (uses no-args + setter and preserves the default).
Expected behavior
Expected behavior (Jackson 2.x)
With Jackson 2.x and this POJO:
- Jackson appears to use the no-args constructor and then setters / field access.
- Since the JSON omits
enabled, the field keeps its default value from the field initializer.
Result at the controller entry point:
userDto.getEnabled() == true
Additional context
No response
Comment From: cowtowncoder
Jackson 3.x is specifically major version upgrade, so in some cases behavioral changes are expected.
I think in this case the change is intentional: all-args constructor is auto-detected by 3.x, since all parameter names are available, constructor is public (visible) and there is just one arguments-taking constructor -- this is typically what most users would want.
Field with default value has no effect on this selection process, I think.
If you want 0-args constructor to be used instead, you will need to either annotate it with @JsonCreator, or lower visibility of all-args constructor to something other than public (or change default Constructor auto-detect visibility to Visibility.NONE).