Skip to content

Jackson 3 requires a public visibility for an implicit long-argument delegating constructor, but not for the equivalent String one (Jackson 2 accepts both as private) #6144

Description

@itineric

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Environment

  • tools.jackson.core:jackson-databind:3.2.1 (jackson-core 3.2.1) — fails with a private constructor, works with a public one
  • com.fasterxml.jackson.core:jackson-databind:2.22.1 (jackson-core 2.22.1, jackson-annotations 2.22) — works with a private constructor
  • No @JsonCreator/@JsonProperty or any other Jackson annotation involved anywhere in the reproduction.
  • Tested with a fully vanilla ObjectMapper/JsonMapper.builder().build() — no custom modules, no custom AnnotationIntrospector, no default typing.

Summary

For a plain POJO with a public no-arg constructor plus two further unannotated, single-argument
constructors — one taking String, one taking long — Jackson 2 implicitly uses either
constructor as a delegating creator depending on the incoming JSON token type (VALUE_STRING or
VALUE_NUMBER_INT), regardless of whether those constructors are private or public.

Jackson 3 keeps this working for the String constructor even when it is private, but for the
long constructor it only works when the constructor is public. With a private long
constructor, deserializing a bare JSON number fails with:

MismatchedInputException: ... although at least one Creator exists ... no int/Int-argument constructor/factory method

We could not find any public configuration knob that restores the old (Jackson 2) behavior while
keeping the long constructor private — see "Things that did not help" below. Making the
constructor public in source is the only thing that fixes it. This asymmetry between the
String and long cases, and the fact that no visibility-related MapperFeature/
VisibilityChecker setting affects it, suggests this is either an unintentional regression from
the 2→3 port, or a hardcoded rule that isn't exposed/documented as configurable.

Version Information

tools.jackson.core:jackson-databind:3.2.1
tools.jackson.core:jackson-core:3.2.1

Reproduction

import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.ObjectMapper;

public class Repro
{
  public static class OneOf
  {
    private boolean isLong;
    private long longValue;
    private String stringValue;

    public OneOf() {}

    // Both constructors are unannotated. Only the visibility of the `long` one matters (see below).
    private OneOf(long value) { this.isLong = true; this.longValue = value; }
    private OneOf(String value) { this.stringValue = value; }

    public boolean isLong() { return isLong; }
    public void setLong(boolean v) { isLong = v; }
    public long getLongValue() { return longValue; }
    public void setLongValue(long v) { longValue = v; }
    public String getStringValue() { return stringValue; }
    public void setStringValue(String v) { stringValue = v; }
  }

  public static void main(String[] args) throws Exception
  {
    ObjectMapper mapper = JsonMapper.builder().build();

    // Works on both Jackson 2 and Jackson 3: uses the private String constructor implicitly.
    OneOf fromString = mapper.readValue("\"abc\"", OneOf.class);
    System.out.println("string ok: " + fromString.getStringValue());

    // Works on Jackson 2, FAILS on Jackson 3 — unless OneOf(long) is made `public`.
    OneOf fromNumber = mapper.readValue("2", OneOf.class);
    System.out.println("number ok: " + fromNumber.getLongValue());
  }
}

The exact same class + a vanilla com.fasterxml.jackson.databind.ObjectMapper (jackson-databind
2.22.1) round-trips both readValue("2", OneOf.class) and readValue("\"abc\"", OneOf.class)
successfully, with OneOf(long) and OneOf(String) both private.

Actual output on jackson-databind 3.2.1 (constructors private)

string ok: abc
Exception in thread "main" tools.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `Repro$OneOf` (although at least one Creator exists): no int/Int-argument constructor/factory method to deserialize from Number value (2)
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN]
	at tools.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:...)
	at tools.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:...)
	at tools.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:...)
	at tools.jackson.databind.deser.ValueInstantiator.createFromInt(ValueInstantiator.java:...)
	at tools.jackson.databind.deser.std.StdValueInstantiator.createFromInt(StdValueInstantiator.java:...)
	at tools.jackson.databind.deser.bean.BeanDeserializerBase.deserializeFromNumber(BeanDeserializerBase.java:...)
	...

Changing only private OneOf(long value) to public OneOf(long value) (leaving OneOf(String)
private) makes both cases succeed on jackson-databind 3.2.1.

Expected behavior

readValue("2", OneOf.class) should succeed the same way it does on Jackson 2, using the long
constructor as an implicit delegating creator regardless of its visibility — consistent with the
String overload, which is picked up implicitly on Jackson 3 even when private.

Additional context

Things that did not help (still fails with the long constructor kept private)

  • MapperFeature.ALLOW_COERCION_OF_SCALARS (enabled)
  • ConstructorDetector.USE_DELEGATING (explicitly set on the JsonMapper.Builder)
  • ConstructorDetector.EXPLICIT_ONLY
  • ConstructorDetector.USE_PROPERTIES_BASED
  • ConstructorDetector.DEFAULT (set explicitly, same as omitting it)
  • MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES (disabled)
  • MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS (enabled explicitly)
  • changeDefaultVisibility(vc -> vc.withCreatorVisibility(Visibility.ANY))

None of these change the outcome — the failure is identical in every case. This is what makes the
String vs long asymmetry notable: whatever gates implicit delegating-creator detection for a
numeric constructor parameter in Jackson 3 does not appear to be one of the standard
visibility/creator-detection configuration surfaces.

Workarounds found

  • Make the long constructor (or an equivalent single-long-arg static factory) public.
  • Or add an explicit @JsonCreator on it (directly, or via a Jackson mix-in if the class must stay
    annotation-free).

Notes / not yet checked

  • We only tested long; we have not checked whether int, double, boolean (or other
    primitive/wrapper types) implicit constructor-based creators are similarly affected, or whether
    this is specific to long/integral numeric types.
  • We have not bisected which exact jackson-databind 3.x release introduced this (only tested
    3.2.1 vs the last 2.x, 2.22.1).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    to-evaluateIssue that has been received but not yet evaluated

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions