Jackson 2.12 Most Wanted (3/5):
Annotation-less @JsonCreator
(even 1-argument)
(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:
- 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.
- 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:
- Current heuristics (for existing code to work as-is)
- Assume properties-based (if
mode
not specified) - Assume delegating (if
mode
not specified) - 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:
SingleArgConstructor
enumeration: matches above-mentioned 4 cases; affects both explicitly annotated cases withoutmode
and auto-detected cases (if allowed)- Require
JsonCreator
annotation? Auto-detection only allowed iffalse
- Allow auto-detection of JDK types (classes in
java.*
andjavax.*
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 overriddenConstructorDetector.USE_PROPERTIES_BASED
: assume that 1-arg constructors withoutmode
definition should all use “properties-based” bindingConstructorDetector.USE_DELEGATING
: assume that 1-arg constructors withoutmode
definition should all use “delegating” bindingConstructorDetector.EXPLICIT_ONLY
: throw an exception on encountering any 1-arg constructor that would be used as creator but does not definemode
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:
- enable auto-detection of Constructors
- for 1-argument constructors specify default mode of
Mode.PROPERTIES_BASED
(explicit annotation can override) - would not auto-detect Constructors of
java.*
orjavax.*
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”) constructorConstructorDetector.withSingleArgMode(SingleArgConstructor)
: if you want an instance with different single-argument modeConstructorDetector.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.