Jackson is one of the most popular libraries (at least according to GitHub stars)
used to handle a variety of tasks related to the JSON format in Java. Among other things Jackson can parse
a JSON file and represent it in memory as an instance of the provided class.
Of course, as long as the structure of data in the file corresponds to the structure of the class.
In all of the examples below, I’ve used the same JSON file that I’ve tried to deserialize to classes with various field
modifiers and methods. All of the examples are available on my GitHub.
| 1
2
3
4
 | {
  "name": "fooName",
  "age": 23
}
 | 
No-arg constructor and field injection
Jackson uses Java reflection under the hood to construct an instance of a given class and then inject values from the
JSON document to the created instance.
By default, Jackson requires a no-arg constructor to instantiate a class. Then the values are injected into fields directly
(if they are public) or through a corresponding setter method. If we don’t provide any additional
information to Jackson the declared field names in our class must match field names in the JSON file. The setters follow
the setX convention.
Public, mutable fields
When our class has a no-arg constructor and public fields that are not final the deserialization process is straightforward
and we don’t need to configure the ObjectMapper in any special way.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 | package com.adebski.jackson;
import java.util.Objects;
public class PersonPublicFieldsNoArgConstructor {
    public PersonPublicFieldsNoArgConstructor() {
        System.out.println("PersonPublicFieldsNoArgConstructor constructor");
    }
    public String name;
    public int age;
     // equals, hashCode, toString, extra methods...
}
 | 
Jackson detects the name and age fields in the class, calls the no-arg constructor through reflection, and injects
values directly into the fields (also through reflection).
This is verified by the following unit test.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 |     @Test
    public void deserializeWithPublicFieldsNoArgConstructor() throws IOException {
        PersonPublicFieldsNoArgConstructor deserializedValue =
            objectMapper.readValue(getSamplePersonFileURL(), PersonPublicFieldsNoArgConstructor.class);
        System.out.println(deserializedValue);
        PersonPublicFieldsNoArgConstructor expectedValue = new PersonPublicFieldsNoArgConstructor();
        expectedValue.name = "fooName";
        expectedValue.age = 23;
        Assertions.assertEquals(expectedValue, deserializedValue);
    }
 | 
Private, mutable fields
Things get more interesting if we have a class with private fields that are still mutable.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 | package com.adebski.jackson;
import java.util.Objects;
public class PersonPrivateFieldsNoArgConstructor {
    public PersonPrivateFieldsNoArgConstructor() {
        System.out.println("PersonPrivateFieldsNoArgConstructor constructor");
    }
    private String name;
    private int age;
    @Override
    public String toString() {
        return "PersonPrivateFieldsNoArgConstructor{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
    }
        
    // equals, hashCode, toString, extra methods...
}
 | 
By default, Jackson does not
“see” non-public fields and throws a com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException exception.
The standard behavior of an ObjectMapper is to throw if a property from a JSON file does not have a corresponding
property in the provided class.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 |     @Test
    public void deserializeWithPrivateFieldsNoArgConstructor() throws IOException {
        UnrecognizedPropertyException unrecognizedPropertyException = Assertions.assertThrows(
            UnrecognizedPropertyException.class,
            () -> objectMapper.readValue(getSamplePersonFileURL(), PersonPrivateFieldsNoArgConstructor.class)
        );
        String expectedMessagePrefix =
            "Unrecognized field \"name\" (class com.adebski.jackson.PersonPrivateFieldsNoArgConstructor), not marked as ignorable (0 known properties: ])";
        Assertions.assertTrue(
            unrecognizedPropertyException.getMessage().startsWith(expectedMessagePrefix)
        );
    }
}
 | 
On the other hand, we can configure our ObjectMapper to consider fields with visibility modifiers
other than public for field injection.
| 1
2
3
4
5
6
7
8
9
 |     @Test
    public void deserializeWithPrivateFieldsNoArgConstructorWithAdditionalConfig() throws IOException {
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        PersonPrivateFieldsNoArgConstructor deserializedValue =
            objectMapper.readValue(getSamplePersonFileURL(), PersonPrivateFieldsNoArgConstructor.class);
        System.out.println(deserializedValue);
        Assertions.assertEquals(PersonPrivateFieldsNoArgConstructor.getExpectedValue(), deserializedValue);
    }
 | 
Here
is a JavaDoc with all the possible values of com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.
Final fields
Since we already know that Jackson can inject into private fields I’ll focus only on the final fields that are also public.
All things in this section apply regardless of the field visibility modifier.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 | package com.adebski.jackson;
import java.util.Objects;
public class PersonPublicFieldsFinalNoArgsConstructor {
    public PersonPublicFieldsFinalNoArgsConstructor() {
        System.out.println("PersonPublicFieldsFinalNoArgsConstructor constructor");
    }
    public final String name = "initialValue";
    public final int age = -25;
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "PersonPublicFieldsFinalNoArgsConstructor{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
    }
    
    // equals, hashCode
}
 | 
Our initial assumption may be that Jackson should not be able to modify fields declared as final in our class,
but it turns out that’s false. Under the hood Jackson modifies fields through one of the set methods of the
java.lang.reflect.Field class. Below is an excerpt
from the set method Javadoc.
If the underlying field is final, the method throws an IllegalAccessException unless setAccessible(true) has succeeded
for this Field object and the field is non-static. Setting a final field in this way is meaningful only during
deserialization or reconstruction of instances of classes with blank final fields, before they are made available
for access by other parts of a program. Use in any other context may have unpredictable effects, including cases
in which other parts of a program continue to use the original value of this field.
What are those “unpredictable effects”? The Java Language Specification 17.5.3
expands on that point.
In some cases, such as deserialization, the system will need to change the final fields of an object after construction.
final fields can be changed via reflection and other implementation-dependent means.
The only pattern in which this has reasonable semantics is one in which an object is constructed and then the final
fields of the object are updated. The object should not be made visible to other threads, nor should the final fields
be read, until all updates to the final fields of the object are complete.
Freezes of a final field occur both at the end of the constructor in which the final field is set,
and immediately after each modification of a final field via reflection or other special mechanism.
Even then, there are a number of complications. If a final field is initialized to a constant expression (ยง15.28) in the
field declaration, changes to the final field may not be observed, since uses of that final field are replaced
at compile time with the value of the constant expression.
If a primitive or a String final field value is assigned directly in the class body (as opposed to writing
the no-arg constructor explicitly) those values are “constant variables” by Java
language (as per 4.12.4 point of the JLS.
In compile-time javac replaces references to such fields with an actual value. We can observe that by using
the javap utility provided by the JDK.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 | javap -p -v target/classes/com/adebski/jackson/PersonPublicFieldsFinalNoArgsConstructor.class
...
// We can see that in the byte code generated for the getter a constant value is returned
  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #2                  // String initialValue
         2: areturn
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/adebski/jackson/PersonPublicFieldsFinalNoArgsConstructor;
...
// And the toString always returns the same value, regardless of the actual value of the fields
  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #9                  // String PersonPublicFieldsFinalNoArgsConstructor{name=\'initialValue\', age=-25}
         2: areturn
      LineNumberTable:
        line 24: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/adebski/jackson/PersonPublicFieldsFinalNoArgsConstructor;
 | 
If we deserialize our JSON document as PersonPublicFieldsFinalNoArgsConstructor we get some surprising results.
Because getters and toString methods have the initial values hardcoded in the
byte-code the instance returned by the ObjectMapper “behaves” as if it hasn’t been modified during deserialization.
At the same time if we access the fields through reflection (or even in IntelliJ debugger) we will see that Jackson
correctly updated the fields.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 |     @Test
    public void deserializeWithPersonPublicFieldsFinalNoArgsConstructorDefaultConfiguration() throws IOException {
        PersonPublicFieldsFinalNoArgsConstructor deserializedValue =
            objectMapper.readValue(getSamplePersonFileURL(), PersonPublicFieldsFinalNoArgsConstructor.class);
        PersonPublicFieldsFinalNoArgsConstructor manuallyConstructedValue = new PersonPublicFieldsFinalNoArgsConstructor();
        System.out.println("Deserialized value: " + deserializedValue);
        System.out.println("Manually constructed value: " + manuallyConstructedValue);
        Assertions.assertEquals(
            manuallyConstructedValue,
            deserializedValue
        );
        System.out.println(
            String.format(
                "Actual name '%s' actual age '%d'",
                getNameThroughReflection(deserializedValue),
                getAgeThroughReflection(deserializedValue)
            )
        );
        Assertions.assertEquals(
            getNameThroughReflection(manuallyConstructedValue),
            manuallyConstructedValue.getName()
        );
        Assertions.assertEquals(
            getAgeThroughReflection(manuallyConstructedValue),
            manuallyConstructedValue.getAge()
        );
        Assertions.assertNotEquals(
            getNameThroughReflection(deserializedValue),
            deserializedValue.getName()
        );
        Assertions.assertNotEquals(
            getAgeThroughReflection(deserializedValue),
            deserializedValue.getAge()
        );
    }
 | 
The test prints
| 1
2
3
4
5
 | PersonPublicFieldsFinalNoArgsConstructor constructor
PersonPublicFieldsFinalNoArgsConstructor constructor
Deserialized value: PersonPublicFieldsFinalNoArgsConstructor{name='initialValue', age=-25}
Manually constructed value: PersonPublicFieldsFinalNoArgsConstructor{name='initialValue', age=-25}
Actual name 'fooName' actual age '23'
 | 
We can configure our ObjectMapper to ignore final fields during deserialization if we’d like to avoid this
“surprising behavior” by setting the ALLOW_FINAL_FIELDS_AS_MUTATORS mapper feature to false. Then
the deserialization results in UnrecognizedPropertyException because Jackson can’t find the fields to inject values to.
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 |     @Test
    public void deserializeWithPersonPublicFieldsFinalNoArgsConstructorDoNotModifyFinalFields() throws IOException {
        objectMapper.configure(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS, false);
        UnrecognizedPropertyException unrecognizedPropertyException = Assertions.assertThrows(
            UnrecognizedPropertyException.class,
            () -> objectMapper.readValue(getSamplePersonFileURL(), PersonPublicFieldsFinalNoArgsConstructor.class),
            "Unrecognized field \"name\" (class com.adebski.jackson.PersonPublicFieldsFinalNoArgsConstructor), not marked as ignorable (0 known properties: ])"
        );
        String expectedMessagePrefix =
            "Unrecognized field \"name\" (class com.adebski.jackson.PersonPublicFieldsFinalNoArgsConstructor), not marked as ignorable (0 known properties: ])";
        Assertions.assertTrue(
            unrecognizedPropertyException.getMessage().startsWith(expectedMessagePrefix)
        );
    }
 | 
Conclusion
Jackson default behavior is to use a no-arg constructor and try to find a way of injecting values to fields one by one. This
may or may not be the behavior we want. We’ve also seen that letting Jackson modify final fields can lead to subtle
bugs which may be hard to track down and understand.
In the next post, I’ll describe the setter injection and highlight some differences between field injection
and setter injection.