Search before asking
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).
Search before asking
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 onecom.fasterxml.jackson.core:jackson-databind:2.22.1(jackson-core 2.22.1, jackson-annotations 2.22) — works with a private constructor@JsonCreator/@JsonPropertyor any other Jackson annotation involved anywhere in the reproduction.ObjectMapper/JsonMapper.builder().build()— no custom modules, no customAnnotationIntrospector, 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 takinglong— Jackson 2 implicitly uses eitherconstructor as a delegating creator depending on the incoming JSON token type (
VALUE_STRINGorVALUE_NUMBER_INT), regardless of whether those constructors areprivateorpublic.Jackson 3 keeps this working for the
Stringconstructor even when it isprivate, but for thelongconstructor it only works when the constructor ispublic. With aprivate longconstructor, deserializing a bare JSON number fails with:
We could not find any public configuration knob that restores the old (Jackson 2) behavior while
keeping the
longconstructorprivate— see "Things that did not help" below. Making theconstructor
publicin source is the only thing that fixes it. This asymmetry between theStringandlongcases, and the fact that no visibility-relatedMapperFeature/VisibilityCheckersetting affects it, suggests this is either an unintentional regression fromthe 2→3 port, or a hardcoded rule that isn't exposed/documented as configurable.
Version Information
tools.jackson.core:jackson-databind:3.2.1tools.jackson.core:jackson-core:3.2.1Reproduction
The exact same class + a vanilla
com.fasterxml.jackson.databind.ObjectMapper(jackson-databind2.22.1) round-trips both
readValue("2", OneOf.class)andreadValue("\"abc\"", OneOf.class)successfully, with
OneOf(long)andOneOf(String)bothprivate.Actual output on jackson-databind 3.2.1 (constructors
private)Changing only
private OneOf(long value)topublic OneOf(long value)(leavingOneOf(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 thelongconstructor as an implicit delegating creator regardless of its visibility — consistent with the
Stringoverload, which is picked up implicitly on Jackson 3 even whenprivate.Additional context
Things that did not help (still fails with the
longconstructor keptprivate)MapperFeature.ALLOW_COERCION_OF_SCALARS(enabled)ConstructorDetector.USE_DELEGATING(explicitly set on theJsonMapper.Builder)ConstructorDetector.EXPLICIT_ONLYConstructorDetector.USE_PROPERTIES_BASEDConstructorDetector.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
Stringvslongasymmetry notable: whatever gates implicit delegating-creator detection for anumeric constructor parameter in Jackson 3 does not appear to be one of the standard
visibility/creator-detection configuration surfaces.
Workarounds found
longconstructor (or an equivalent single-long-arg static factory)public.@JsonCreatoron it (directly, or via a Jackson mix-in if the class must stayannotation-free).
Notes / not yet checked
long; we have not checked whetherint,double,boolean(or otherprimitive/wrapper types) implicit constructor-based creators are similarly affected, or whether
this is specific to
long/integral numeric types.3.2.1 vs the last 2.x, 2.22.1).