FasterXML / FasterXML/jackson-databind
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`)
- Dominant language
- Java
- Stars
- 3.7k
- Forks
- 1.5k
- Avg merge
- 3d 6h
- Merged PRs (30d)
- 28
Description
### Search before asking
- [x] I searched in the [issues](https://github.com/FasterXML/jackson-databind/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
```java
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).
Contributor guide
No contributing guide indexed for this repository
Research direction
Reproduce the failure with the vanilla JsonMapper example, then trace the creator selection through ValueInstantiator, StdValueInstantiator, and BeanDeserializerBase named in the stack trace. Compare Jackson 3's handling of the private long and String constructors with Jackson 2, and add regression coverage showing the expected private numeric-constructor behavior across relevant primitive types.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100