There is a simple class with parameterized constructor. I'm looking for a way to apply it for JSON deserialization with minimal annotations usage.
How to make Jackson recognize id
field by its name without explicit JsonProperty
?
Class:
public final class MyRec {
final String id;
public MyRec1(String id) {
this.id = id;
}
}
Deserialization fails unless annotation @JsonProperty("id")
is present on the field.
Deserialization attempt:
new ObjectMapper().readValue("""{"id":"abc"}""", MyRec.class);
Exception:
Cannot construct instance of `sample.MyRec` (although at least one Creator exists):
cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"id":"abc"}"; line: 1, column: 2]
com.fasterxml.jackson.databind.exc.MismatchedInputException:
Cannot construct instance of `sample.MyRec` (although at least one Creator exists):
cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"id":"abc"}"; line: 1, column: 2]
at app//com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
To make Jackson capable of detecting the required constructor based on the number of properties without making use of data-binding annotations @JsonCreator
and @JsonProperty
you can add a dependency to Jackson Modules: Java 8 (a link to the Maven repository).
Jackson Modules: Java 8 is an umbrella multi-module which includes Parameter names module.
If you are using Spring Boot, the only thing needed to register the module is to place ParameterNamesModule
as a Bean into the Spring's Context, and it would be grabbed at the application start-up and registred automatically by the JacksonAutoConfiguration
while ObjectMapper
would be configured.
As explained here you need to provide JsonCreator.Mode.PROPERTIES
as argument while instantiating the module.
@Bean
public ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.PROPERTIES);
}
If you're not using Spring in your project, then you would need to configure ObjectMapper
manually:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES));
There are caveats. Specifically there's an issue with detecting a single-arg constructor. Here's a quote from the module-description:
Preconditions:
- class Person must be compiled with Java 8 compliant compiler with option to store formal parameter names turned on (-parameters option). For more information about Java 8 API for accessing parameter names at runtime see this
- if there are multiple visible constructors and there is no default constructor, @JsonCreator is required to select constructor for deserialization
- if used with jackson-databind lower than 2.6.0, @JsonCreator is required. In practice, @JsonCreator is also often required with 2.6.0+ due to issues with constructor discovery that will be resolved in 2.7.
- if class
Person
has a single argument constructor, its argument needs to be annotated with@JsonProperty("propertyName")
. This is to preserve legacy behavior, see FasterXML/jackson-databind/#1498
So, to make the Jackson recognize a single-arg constructor, we need to apply @JsonProperty
on the argument.
Here's also a mention of this issue JsonCreator.Mode.PROPERTIES
documentation:
Mode that indicates that the argument(s) for creator are to be bound from matching properties of incoming Object value, using creator argument names (explicit or implicit) to match incoming Object properties to arguments.
Note that this mode is currently (2.5) always used for multiple-argument creators; the only ambiguous case is that of a single-argument creator.
This behavior might be fixed with Jackson 3 as mentioned here.
If you're using JDK 16+ in your project, then you can make this final class to be a record.
Records doesn't declare a default no-args constructor, instead the compiler generates a so-called canonical constructor, declaring all the arguments from the record's header (refer to the JLS 8.10.4. Record Constructor Declarations for more information).
And Jackson uses canonical constructor as a default creator to instantiate records.
The following record would be desirialized without issues:
public record MyRec(String id) {}