Search code examples
javajsonjacksonjackson-databind

How to make Jackson recognize constructor field names as properties without explicit JsonProperty?


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)

Solution

  • Parameter names module

    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));
    

    Note

    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.

    Java 16 Records

    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) {}