1. Introduction

One of the essential aspects of working with Jackson is understanding how it maps JSON data to Java objects, which often involves using constructors. Besides, the ConstructorDetector is a key component in Jackson that influences how constructors are selected during deserialization.

In this tutorial, we’ll explore the ConstructorDetector in detail, explaining its purpose, configurations, and usage.

2. The Constructordetector: an Overview

ConstructorDetector is a feature in Jackson’s data binding module that helps determine which constructors of a class are considered for object creation during deserialization. Jackson uses constructors to instantiate objects and populate their fields with JSON data.

The ConstructorDetector allows us to customize and control which constructors Jackson should use, providing more flexibility in the deserialization process.

3. Configuring the ConstructorDetector

Jackson provides several predefined ConstructorDetector configurations, including USE_PROPERTIES_BASED, USE_DELEGATING, EXPLICIT_ONLY, and DEFAULT.

3.1. USE_PROPERTIES_BASED

This configuration is useful when our class has a constructor that matches the JSON properties. Let’s take a simple practical example:

public class User {
    private String firstName;
    private String lastName;
    private int age;

    public User(){
    }

    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public String getFirstName() {
        return firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public int getAge() {
        return age;
    }
}

In this scenario, the class User has the properties firstName, lastName, and age. Jackson will look for a constructor in User with parameters that match these properties, which it finds in the provided User(String firstName, String lastName, int age) constructor.

Now, when deserializing JSON to a Java object using Jackson with ConstructorDetector.USE_PROPERTIES_BASED, Jackson will utilize the constructor that best matches the properties in the JSON object:

@Test
public void givenUserJson_whenUsingPropertiesBased_thenCorrect() throws Exception {
    String json = "{\"firstName\": \"John\", \"lastName\": \"Doe\", \"age\": 25}";

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

    User user = mapper.readValue(json, User.class);
    assertEquals("John", user.getFirstName());
    assertEquals(25, user.getAge());
}

Here, the string named json represents a JSON object with properties firstName, lastName, and age, which correspond to the constructor parameters of the User class. When deserializing the JSON using the mapper.Jackson will use the readValue(json, User.class) method to utilize the constructor with parameters matching the JSON properties.

If the JSON contains additional fields that aren’t present in the class, Jackson will ignore those fields without throwing an error. For example:

String json = "{\"firstName\": \"John\", \"lastName\": \"Doe\", \"age\": 25, \"extraField\": \"extraValue\"}";
User user = mapper.readValue(json, User.class);

In this case, the extraField is ignored. However, if the constructor parameters don’t match exactly with the JSON properties, Jackson may fail to find a suitable constructor and throw an error.

3.2. USE_DELEGATING

The USE_DELEGATING configuration allows Jackson to delegate object creation to a single-argument constructor. This can be beneficial when the JSON data structure aligns with the structure of a single parameter, enabling concise object creation.

Consider a use case where we have a class StringWrapper that wraps a single string value:

public class StringWrapper {
    private String value;

    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public StringWrapper(@JsonProperty("value") String value) {
        this.value = value;
    }

    @JsonProperty("value")
    public String getValue() {
        return value;
    }
}

The StringWrapper class has a single-parameter constructor annotated with @JsonCreator and @JsonProperty, indicating that Jackson should use delegation for object creation.

Let’s deserialize JSON to a Java object using Jackson with ConstructorDetector.USE_DELEGATING:

@Test
public void givenStringJson_whenUsingDelegating_thenCorrect() throws Exception {
    String json = "\"Hello, world!\"";

    ObjectMapper mapper = JsonMapper.builder()
      .constructorDetector(ConstructorDetector.USE_DELEGATING)
      .build();

    StringWrapper wrapper = mapper.readValue(json, StringWrapper.class);
    assertEquals("Hello, world!", wrapper.getValue());
}

Here, we deserialize a JSON string value “*Hello, world!*” to a StringWrapper object using Jackson with ConstructorDetector.USE_DELEGATING. Jackson utilizes the single-parameter constructor of StringWrapper, correctly mapping the JSON string value.

Jackson will throw an error if the JSON structure doesn’t align with the single-parameter constructor. For example:

String json = "{\"value\": \"Hello, world!\", \"extraField\": \"extraValue\"}";
StringWrapper wrapper = mapper.readValue(json, StringWrapper.class);

In this case, the additional field extraField causes an error because the constructor expects a single string value, not a JSON object.

3.3. EXPLICIT_ONLY

This configuration ensures only explicitly annotated constructors are used. Furthermore, it provides strict control over constructor selection, allowing developers to specify which constructors Jackson should consider during deserialization.

Consider a scenario where the class Product represents a product with a name and price:

public class Product {
    private String value;
    private double price;

    @JsonCreator
    public Product(@JsonProperty("value") String value, @JsonProperty("price") double price) {
        this.value = value;
        this.price = price;
    }

    public String getName() {
        return value;
    }

    public double getPrice() {
        return price;
    }
}

This class has a constructor annotated with @JsonCreator, indicating that Jackson should use explicit constructor-based instantiation during deserialization.

Let’s see the deserialization process:

@Test
public void givenProductJson_whenUsingExplicitOnly_thenCorrect() throws Exception {
    String json = "{\"value\": \"Laptop\", \"price\": 999.99}";

    ObjectMapper mapper = JsonMapper.builder()
      .constructorDetector(ConstructorDetector.EXPLICIT_ONLY)
      .build();

    Product product = mapper.readValue(json, Product.class);
    assertEquals("Laptop", product.getName());
    assertEquals(999.99, product.getPrice(), 0.001);
}

In this test method, we utilize the ConstructorDetector.EXPLICIT_ONLY configuration to deserialize a JSON object representing a product to a Product object. Only the annotated constructor of the Product class will be considered.

Jackson will throw an error if the JSON object has additional fields not present in the constructor or is missing required fields. For example:

String json = "{\"value\": \"Laptop\"}";
Product product = mapper.readValue(json, Product.class);

This will result in an error because the price field is missing.

String json = "{\"value\": \"Laptop\", \"price\": 999.99, \"extraField\": \"extraValue\"}";
Product product = mapper.readValue(json, Product.class);

This will also result in an error due to an unexpected field extraField.

3.4. DEFAULT

The DEFAULT configuration provides a balanced approach by considering various strategies for constructor selection. This type of configuration aims to select constructors that align with the JSON structure while considering custom annotations and other configuration options.

Consider a scenario where a class Address represents a postal address:

public class Address {
    private String street;
    private String city;

    public Address(){
    }
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }
}

The Address class has a constructor with parameters matching the JSON properties street and city.

Let’s deserialize JSON to a Java object using Jackson with ConstructorDetector.DEFAULT:

@Test
public void givenAddressJson_whenUsingDefault_thenCorrect() throws Exception {
    String json = "{\"street\": \"123 Main St\", \"city\": \"Springfield\"}";

    ObjectMapper mapper = JsonMapper.builder()
      .constructorDetector(ConstructorDetector.DEFAULT)
      .build();

    Address address = mapper.readValue(json, Address.class);
    assertEquals("123 Main St", address.getStreet());
    assertEquals("Springfield", address.getCity());
}

Jackson employs its default heuristic to select the constructor that matches the JSON structure, ensuring accurate object instantiation from JSON data.

If the JSON structure is more complex or includes nested objects that don’t directly match any constructor, Jackson may not find a suitable constructor and throw an error. For example:

String json = "{\"street\": \"123 Main St\", \"city\": \"Springfield\", \"extraField\": \"extraValue\"}";
Address address = mapper.readValue(json, Address.class);

In this case, the extraField could cause an error if the default configuration can’t handle it.

4. Conclusion

In conclusion, understanding the purpose and configurations of ConstructorDetector in Jackson is crucial for the effective mapping of JSON data to Java objects during deserialization.

As always, the complete code samples for this article can be found over on GitHub.