Jackson 2.12 Most Wanted (3/5):

Annotation-less @JsonCreator (even 1-argument)

@cowtowncoder
5 min readDec 4, 2020

(part 3 of “Deeper Dive on Jackson 2.12” mini-series — see “Jackson 2.12 Features” for context)

Another long-requested feature — jackson-databind#1498 — was finally included in 2.12 as well, by introducing a new configuration setting called CosntructorDetector along with couple of default implementations to use.

TL;DNR

The likeliest usage of this feature is something like:

ObjectMapper mapper = JsonMapper.builder()
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
.build()

but what exactly this does will require bit longer explanation.

Background: what is the problem

To understand the feature itself, let’s start with the problem being solved.
Consider this explicitly annotated POJO:

public class Point {
private int x, y;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public Point(@JsonProperty("x") int x, @JsonProperty("y") int x) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}

While this works, many users would prefer having to use fewer — or ideally, zero — annotations to indicate that given constructor should be used for creating an instance of value type, Point.

Let’s start with @JsonProperty annotations: these are not usually needed for getters or setters, but since JDK used to not store parameter names for methods, they were required for constructor arguments.
But with Java 8 this information has been available (assuming javac is configured to include names in bytecode) and by registering Jackson module jackson-module-parameter-names (from jackson-modules-java8 github repo) this information becomes available to use by Jackson — meaning that @JsonProperty annotations are no longer required for constructor (or factory method) arguments.

But what about @JsonCreator annotation? If there is only one visible constructor (by default only public constructors are considered visible, except for 0-argument “default” constructor which can have any visibility) and if it has names for properties, it will be detected.
So above example DOES NOT require any annotations if byte code is compiled with parameter names AND jackson-module-parameter-names is registered with ObjectMapper!

There is, however, one exception — that of 1-argument constructors. These constructors will not be auto-detected. Why? Because 1-argument constructors (and static factory methods) are ambiguous: there are 2 possible “modes” of binding:

  1. Properties-based: input must be JSON Object, and properties match by name with constructor arguments (which is why names must be in byte code OR annotated, as above); property value will be deserialized into type indicated by matching argument. This mode works for all constructors, regardless of number of arguments.
  2. Delegating: input can be any JSON type and will be deserialized into type of the one (and only) constructor argument — its name is irrelevant but type must match. This mode works only for constructor with 1 argument (actually there are special cases that use @JacksonInject and the exact rule is that “one and only one parameter that is not injected”).

More concretely here are some examples of alternate usage:

// accepts JSON value like: "some text"
// and serializes back to JSON String
public class StringWrapper {
private String value;
@JsonCreator(mode = JsonCreator.MODE.DELEGATING)
public StringWrapper(String v) {
value = v;
}
// commonly serialized like so:
@JsonValue
public String getValue() {
return value;
}
}
// accepts JSON value like: { "id" : "xc974" }
public class IdToken {
private String id;
@JsonCreator(mode = JsonCreator.MODE.PROPERTIES)
public IdToken(@JsonProperty(id") String id) {
this.id = id;
}
// will serialize as JSON object with this property:
public String getId() { return id; }
}

Now: if @JsonCreator declaration was missing mode property, Jackson would try to figure out more likely of two modes — and the heuristics have proven quite unreliable leading to issues with users being confused as to why specific JSON content does not match.
Due to this reason, 1-argument constructors have not been auto-detected without explicit annotation until Jackson 2.12.

But users have indicated that they would really, REALLY like to be able to omit that annotation too… so I figured that I really should find a way to solve this problem, in a way that does not add more fragility or confusion.

The Solution: ConstructorDetector

One possible solution here could have been to add new MapperFeature for users to enable. However, even for deciding between “properties-based” and “delegating” cases (for 1-argument constructor), we have more than 2 options to consider:

  1. Current heuristics (for existing code to work as-is)
  2. Assume properties-based (if mode not specified)
  3. Assume delegating (if mode not specified)
  4. Refuse to guess, fail (if mode not specified)

and more generally, some users might not event want to allow auto-detection for constructors. As a result, I designed a new configuration object type — ConstructorDetector — which contains 3 settings:

  1. SingleArgConstructor enumeration: matches above-mentioned 4 cases; affects both explicitly annotated cases without mode and auto-detected cases (if allowed)
  2. Require JsonCreator annotation? Auto-detection only allowed if false
  3. Allow auto-detection of JDK types (classes in java.* and javax.* packages)?

Of these, (2) and (3) are mostly used for security reasons: enabling (3) in particular could lead to security concerns if polymorphic deserialization with class as type id is enabled (especially via “Default Typing”).

To simplify the common use cases there are actually 4 pre-defined ConstructorDetector instances to use. All pre-defined options have common settings for:

  • JsonCreator annotation NOT required (that is: auto-detection is enabled)
  • Auto-detection for JDK types is NOT allowed, however (for security reasons)

so the only variation is handling of mode for auto-detected constructors.
With that, naming of instances should be straightforward:

  • ConstructorDetector.DEFAULT: approximates current (pre-2.12) Jackson settings — is the default setting unless overridden
  • ConstructorDetector.USE_PROPERTIES_BASED: assume that 1-arg constructors without mode definition should all use “properties-based” binding
  • ConstructorDetector.USE_DELEGATING: assume that 1-arg constructors without mode definition should all use “delegating” binding
  • ConstructorDetector.EXPLICIT_ONLY: throw an exception on encountering any 1-arg constructor that would be used as creator but does not define mode

Actual usage

As mentioned earlier on, the new setting will be set using builder-style construction of ObjectMapper:

ObjectMapper mapper = JsonMapper.builder()
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
.build()

and would:

  1. enable auto-detection of Constructors
  2. for 1-argument constructors specify default mode of Mode.PROPERTIES_BASED (explicit annotation can override)
  3. would not auto-detect Constructors of java.* or javax.* types

This is also the most likely configuration setting most users would want to use.

Advanced use

In case you want to change settings of ConstructorDetector, there are a few “mutant factory” methods:

  • ConstructorDetector.withRequireAnnotation(boolean): if you want to enforce use of @JsonCreator to find any constructor other than 0-argument (“default”) constructor
  • ConstructorDetector.withSingleArgMode(SingleArgConstructor): if you want an instance with different single-argument mode
  • ConstructorDetector.withAllowJDKTypeConstructors(boolean): if you REALLY want to auto-detect constructors from JDK types (not recommended)

but chances are that only the first one is of use currently. This may change if new options are added in 2.13 or later versions.

--

--

@cowtowncoder

Open Source developer, most known for Jackson data processor (nee “JSON library”), author of many, many other OSS libraries for Java, from ClassMate to Woodstox