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

  1. Use Jackson 2.x (com.fasterxml.jackson.databind) with the UserDto class above.
  2. POST the JSON shown above to the controller that takes @RequestBody UserDto.
  3. Observe userDto.getEnabled() at the start of the controller method.
  4. Upgrade to Jackson 3.x (tools.jackson.databind) keeping the same code and JSON.
  5. 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 enabled property, the enabled parameter of the all-args constructor is null.
  • 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).