Most of the time, while working with Jackson within a Spring Boot project, I encountered an issue Which I would like to share with you all.
What is Jackson?
Jackson is a suite of data-processing tools for Java comprising of three components:
Streaming (jackson-core) defines low-level streaming APIs and includes JSON-specific implementations.
Annotations (jackson-annotations) contains standard Jackson annotations.
Databind (jackson-databind) implements data-binding (and object serialization) support on the streaming package. This package depends on both the streaming and annotations packages.
Jackson is currently the leading option for parsing JSON in Java. The
Jackson library is composed of three components: Jackson Databind, Core, and
Annotation. Jackson Databind has internal dependencies on Jackson Core and Annotation. Therefore, adding Jackson Databind to your Maven POM dependency list will include the other dependencies as well. To use the latest Jackson library, you need to add the following dependency in the Maven POM.
The below dependency works well in other Spring projects, Just cross verify the version of spring boot , accordingly jackson-databind version may change.
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
Reading JSON – Data Binding in Jackson
Data binding is a JSON processing model that allows for seamless conversion between JSON data and Java objects. With data binding, you create POJOs following
JavaBeans convention with properties corresponding to the JSON data. The Jackson
ObjectMapper is responsible for mapping the
JSON data to the POJOs.
The Jackson JSON toolkit contains a set of Java annotations which you can use to influence how JSON is read into
objects, or what JSON is generated from the objects. This Jackson annotation tutorial explains how to use
Jackson's annotations.
The Jackson JSON toolkit
contains a set of Java annotations which you can use to influence how JSON is
read into objects, or what JSON is generated from the objects. This Jackson
annotation tutorial explains how to use Jackson's annotations.
Jackson contains a set of
annotations that affect both the reading of Java objects from JSON, as well as
the writing of Java objects into JSON. I refer to these annotations as
"read + write annotations". The following sections explain Jackson's read
+ write annotations in more detail.
The Jackson annotation @JsonIgnore is used to tell Jackson to ignore a certain property (field) of a Java
object. The property is ignored both when reading JSON into Java objects, and
when writing Java objects into JSON. Here is an example class that uses the @JsonIgnore annotation:
import com.fasterxml.jackson.annotation.JsonIgnore;
public class PersonIgnore {
@JsonIgnore
public long personId = 0;
public String name = null;
}
In the above class the
property personId will not be read from JSON or written to JSON.
The @JsonIgnoreProperties Jackson annotation is used to specify a list of properties of a class
to ignore. The @JsonIgnoreProperties annotation is placed above
the class declaration instead of above the individual properties (fields) to
ignore. Here is an example showing how to use the @JsonIgnoreProperties annotation:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties({"firstName", "lastName"})
public class PersonIgnoreProperties {
public long personId = 0;
public String firstName = null;
public String lastName
= null;
}
In this example both the
properties firstName and lastName will be ignored because their names are listed inside the @JsonIgnoreProperties annotation declaration above the class declaration.
The @JsonIgnoreType Jackson annotation is used to mark a whole type (class) to be ignored
everywhere that type is used. Here is an example that shows you how you could
use the @JsonIgnoreType annotation:
import com.fasterxml.jackson.annotation.JsonIgnoreType;
public class PersonIgnoreType {
@JsonIgnoreType
public static class Address {
public String
streetName = null;
public String houseNumber
= null;
public String zipCode = null;
public String city = null;
public String country = null;
}
public long personId = 0;
public String name = null;
public Address address = null;
}
In the above example all Address instances will be ignored.
The Jackson annotation @JsonAutoDetect is used to tell Jackson to include properties which are not public,
both when reading and writing objects. Here is an example class showing you how
you can use the @JsonAutoDetect annotation:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY )
public class PersonAutoDetect {
private long personId = 123;
public String name = null;
}
The JsonAutoDetect.Visibility class contains constants matching the visibility levels in Java,
meaning ANY, DEFAULT, NON_PRIVATE, NONE, PROTECTED_AND_PRIVATE and PUBLIC_ONLY.
Jackson contains a set of
annotations that only affect how Jackson parses JSON into objects - meaning
they affect Jackson's reading of JSON. I refer to these as "read
annotations". The following sections explains Jackson's read annotations.
The Jackson annotation @JsonSetter is used to tell Jackson that is should match the name of this setter
method to a property name in the JSON data, when reading JSON into objects.
This is useful if the property names used internally in your Java class is not
the same as the property name used in the JSON file.
The following Person class uses the name personId for its id property:
public class Person {
private long personId = 0;
private String name = null;
public long getPersonId() {
return this.personId; }
public void setPersonId(long
personId) { this.personId = personId; }
public String getName() {
return name; }
public void setName(String
name) { this.name = name; }
}
But in this JSON object the
name id is used instead of personId:
{
"id" : 1234,
"name" :
"John"
}
Without some help Jackson
cannot map the id property from the JSON object to the personId field of
the Java class.
The @JsonSetter annotation instructs Jackson to use a setter method for a given JSON
field. In our case we add the @JsonSetter annotation
above the setPersonId() method. Here is how adding the @JsonSetter annotation looks like:
public class Person {
private long personId = 0;
private String name = null;
public long getPersonId() {
return this.personId; }
@JsonSetter("id")
public void setPersonId(long
personId) { this.personId = personId; }
public String getName() {
return name; }
public void setName(String
name) { this.name = name; }
}
The value specified inside the
@JsonSetter annotation is the name of the JSON field to
match to this setter method. In this case the name is id since that
is the name of the field in the JSON object we want to map to the setPersonId() setter method.
The Jackson annotation @JsonAnySetter instructs Jackson to call the same setter method for all unrecognized
fields in the JSON object. By "unrecognized" I mean all fields that
are not already mapped to a property or setter method in the Java object. Look
at this Bag class:
public class Bag {
private Map<String,
Object> properties = new HashMap<>();
public void set(String
fieldName, Object value){
this.properties.put(fieldName, value);
}
public Object get(String fieldName){
return
this.properties.get(fieldName);
}
}
And then look at this JSON
object:
{
"id" : 1234,
"name" :
"John"
}
Jackson cannot directly map
the id and name property of this JSON object to the Bag class,
because the Bag class contains no public fields or setter methods.
You can tell Jackson to call
the set() method for all unrecognized fields by adding the @JsonAnySetter annotation, like this:
public class Bag {
private Map<String,
Object> properties = new HashMap<>();
@JsonAnySetter
public void set(String
fieldName, Object value){
this.properties.put(fieldName, value);
}
public Object get(String
fieldName){
return
this.properties.get(fieldName);
}
}
Now Jackson will call the set() method with the name and value of all unrecognized fields in the JSON
object.
Keep in mind that this only
has effect on unrecognized fields. If, for instance, you added a public name property or setName(String) method to
the Bag Java class, then the name field in the JSON object
would be mapped to that property / setter instead.
The Jackson annotation @JsonCreator is used to tell Jackson that the Java object has a constructor (a
"creator") which can match the fields of a JSON object to the fields
of the Java object.
The @JsonCreator annotation is useful in situations where the @JsonSetter annotation cannot be used. For instance, immutable objects do not have
any setter methods, so they need their initial values injected into the constructor.
Look at this PersonImmutable class as example:
public class PersonImmutable {
private long id =
0;
private String name = null;
public PersonImmutable(long
id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
}
To tell Jackson that it should
call the constructor of PersonImmutable we must add
the @JsonCreator annotation to the constructor. But that alone
is not enough. We must also annotate the parameters of the constructor to tell
Jackson which fields from the JSON object to pass to which constructor
parameters. Here is how the PersonImmutable class looks
with the @JsonCreator and @JsonProperty annotations
added:
public class PersonImmutable {
private long id =
0;
private String name = null;
@JsonCreator
public PersonImmutable(
@JsonProperty("id")
long id,
@JsonProperty("name")
String name ) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
}
Notice the annotation above
the constructor and the annotations before the constructor parameters. Now
Jackson is capable of creating a PersonImmutable from this
JSON object:
{
"id" : 1234,
"name" :
"John"
}
The Jackson annotation @JacksonInject is used to inject values into the parsed objects, instead of reading
those values from the JSON. For instance, imagine you are downloading person
JSON objects from various different sources, and would like to know what source
a given person object came from. The sources may not themselves contain that
information, but you can have Jackson inject that into the Java objects created
from the JSON objects.
To mark a field in a Java
class as a field that needs to have its value injected by Jackson, add the @JacksonInject annotation above the field. Here is an example PersonInject class with the @JacksonInject annotation
added above the source field:
public class PersonInject {
public long id =
0;
public String name = null;
@JacksonInject
public String source = null;
}
In order to have Jackson
inject values into the source field you need to do a little
extra when creating the Jackson ObjectMapper. Here is
what it takes to have Jackson inject values into the Java objects:
InjectableValues inject = new
InjectableValues.Std().addValue(String.class, "jenkov.com");
PersonInject personInject = new ObjectMapper().reader(inject)
.forType(PersonInject.class)
.readValue(new File("data/person.json"));
Notice how the value to inject
into the source attribute is set in the InjectableValues addValue() method. Notice also that the value is only tied to the type String - not to any specific field name. It is the @JacksonInject annotation that specifies what field the value is to be injected into.
If you were to download person
JSON objects from multiple sources and have a different source value injected
for each source, you would have to repeat the above code for each source.
The Jackson annotation @JsonDeserialize is used to specify a custom de-serializer class for a given field in a
Java object. For instance, imagine you wanted to optimize the on-the-wire
formatting of the boolean values false and true to 0 and 1.
First you would need to add
the @JsonDeserialize annotation to the field you want to use the
custom deserializer for. Here is how adding the @JsonDeserialize annotation to a field looks like:
public class PersonDeserialize {
public long id
= 0;
public String name
= null;
@JsonDeserialize(using =
OptimizedBooleanDeserializer.class)
public boolean enabled =
false;
}
Second, here is what the OptimizedBooleanDeserializer class referenced from the @JsonDeserialize annotation
looks like:
public class OptimizedBooleanDeserializer
extends
JsonDeserializer<Boolean> {
@Override
public Boolean
deserialize(JsonParser jsonParser,
DeserializationContext
deserializationContext) throws
IOException, JsonProcessingException
{
String text =
jsonParser.getText();
if("0".equals(text)) return false;
return true;
}
}
Notice that the OptimizedBooleanDeserializer class extends JsonDeserializer with the
generic type Boolean . Doing so makes the deserialize() method
return a Boolean object. If you were to deserialize another type (e.g a java.util.Date) you would have to specify that type inside the generics brackets
You obtain the value of the
field to deserialize by calling the getText() method of the jsonParser parameter. You can then deserialize that text into whatever value and
type your deserializer is targeted at (a Boolean in this
example).
Finally you need to see what
it looks like to deserialize an object with a custom deserializer and the @JsonDeserializer annotation:
PersonDeserialize person = objectMapper
.reader(PersonDeserialize.class)
.readValue(new
File("data/person-optimized-boolean.json"));
Notice how we first need to
create a reader for the PersonDeserialize class using
the ObjectMapper's reader() method, and then we call readValue() on the object returned by that method.
Jackson also contains a set of
annotations that can influence how Jackson serializes (writes) Java objects to
JSON. Each of these write (serialization) annotations will be covered in the
following sections.
The Jackson annotation @JsonInclude tells Jackson only to include properties under certain circumstances.
For instance, that properties should only be included if they are non-null,
non-empty, or have non-default values. Here is an example that shows how you
can use the @JsonInclude annotation:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class PersonInclude {
public long personId = 0;
public String name = null;
}
This example will only include
the name property if the value set for it is non-empty, meaning is not null and
is not an empty string.
A more saying name for the @JsonInclude annotation could have been @JsonIncludeOnlyWhen, but that
would have been longer to write.
The @JsonGetter Jackson annotation is used to tell Jackson that a certain field value
should be obtained from calling a getter method instead of via direct
field access. The @JsonGetter annotation is useful if your
Java class uses jQuery style for getter and setter names. For instance, instead
of getPersonId() and setPersonId() you might
have the methods personId() and personId(long id).
Here is an example class named
PersonGetter which shows the use of the @JsonGetter annotation:
public class PersonGetter {
private long personId = 0;
@JsonGetter("id")
public long personId() {
return this.personId; }
@JsonSetter("id")
public void personId(long
personId) { this.personId = personId; }
}
As you can see, the personId() method is annotated with the @JsonGetter annotation. The value set on the @JsonGetter annotation is the name that should be used in the JSON object. Thus,
the name used for the personId in the JSON object is id. The resulting JSON object would look like this:
{"id":0}
Notice also that the personId(long
personId) method is annotated with the @JsonSetter annotation to make Jackson recognized that as the setter matching the id attribute in the JSON object. The @JsonSetter annotation is used when reading from JSON into Java objects - not when
writing Java objects to JSON. The @JsonSetter annotation
is just included for the sake of completeness.
The @JsonAnyGetter Jackson annotation enables you to use a Map as
container for properties that you want to serialize to JSON. Here is an example
of using the @JsonAnyGetter annotation in a Java class:
public class PersonAnyGetter {
private Map<String,
Object> properties = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> properties() {
return properties;
}
}
When seeing the @JsonAnyGetter annotation Jackson will obtain the Map returned
from the method which @JsonAnyGetter annotates,
and will treat each key-value pair in that Map as a
property. In other words, all key-value pairs in the Map will be
serialized to JSON as part of the PersonAnyGetter object.
The @JsonPropertyOrder Jackson annotation can be used to specify in what order the fields of
your Java object should be serialized into JSON. Here is an example showing how
to use the @JsonPropertyOrder annotation:
@JsonPropertyOrder({"name", "personId"})
public class PersonPropertyOrder {
public long personId
= 0;
public String name = null;
}
Normally Jackson would have
serialized the properties in PersonPropertyOrder in the
sequence they are found in the class. However, the @JsonPropertyOrder annotation specifies a different order where the name property
will be appear first and the personId property second in the
serialized JSON output.
The @JsonRawValue Jackson annotation tells Jackson that this property value should
written directly as it is to the JSON output. If the property is a String Jackson would normally have enclosed the value in quotation marks, but
if annotated with the @JsonRawValue property Jackson won't do
that.
To make it more clear what @JsonRawValue does, look at this class without the @JsonRawValue in use:
public class PersonRawValue {
public long personId = 0;
public String address = "$#";
}
Jackson would serialize this
into this JSON string:
{"personId":0,"address":"$#"}
Now we add the @JsonRawValue to the address property, like this:
public class PersonRawValue {
public long personId = 0;
@JsonRawValue
public String address = "$#";
}
Jackson will now omit the
quotation marks when serializing the address property.
The serialized JSON will thus look like this:
{"personId":0,"address":$#}
This is of course invalid
JSON, so why would you want that?
Well, if the address property contained a JSON string then that JSON string would be
serialized into the final JSON object as part of the JSON object structure, and
not just into a string in the address field in the JSON object. To
see how that would work, let us change the value of the address property
like this:
public class PersonRawValue {
public long personId = 0;
@JsonRawValue
public String address =
"{
\"street\" : \"Wall Street\", \"no\":1}";
}
Jackson would serialize this
into this JSON:
{"personId":0,"address":{ "street" :
"Wall Street", "no":1}}
Notice how the JSON string is
now part of the serialized JSON structure.
Without the @JsonRawValue annotation Jackson would have serialized the object to this JSON:
{"personId":0,"address":"{ \"street\"
: \"Wall Street\", \"no\":1}"}
Notice how the value of the address property is now enclosed in quotation marks, and all the quotation
marks inside the value are escaped.
The Jackson annotation @JsonValue tells Jackson that Jackson should not attempt to serialize the object
itself, but rather call a method on the object which serializes the object to a
JSON string. Note that Jackson will escape any quotation marks inside the
String returned by the custom serialization, so you cannot return e.g. a full
JSON object. For that you should use @JsonRawValue instead (see previous section).
The @JsonValue annotation is added to the method that Jackson is to call to serialize
the object into a JSON string. Here is an example showing how to use the @JsonValue annotation:
public class PersonValue {
public long personId = 0;
public String name = null;
@JsonValue
public String toJson(){
return this.personId +
"," + this.name;
}
}
The output you would get from
asking Jackson to serialize a PersonValue object is
this:
"0,null"
The quotation marks are added
by Jackson. Remember, any quotation marks in the value string returned by the
object are escaped.
The @JsonSerialize Jackson annotation is used to specify a custom serializer for a field
in a Java object. Here is an example Java class that uses the @JsonSerialize annotation:
public class PersonSerializer {
public long personId = 0;
public String name = "John";
@JsonSerialize(using =
OptimizedBooleanSerializer.class)
public boolean enabled =
false;
}
Notice the @JsonSerialize annotation above the enabled field.
The OptimizedBooleanSerializer will serialize a true value to 1 and a false value 0. Here is the code:
public class OptimizedBooleanSerializer extends
JsonSerializer<Boolean> {
@Override
public void serialize(Boolean
aBoolean, JsonGenerator jsonGenerator,
SerializerProvider
serializerProvider)
throws IOException, JsonProcessingException
{
if(aBoolean){
jsonGenerator.writeNumber(1);
} else {
jsonGenerator.writeNumber(0);
}
}
}
General Annotations
The general annotations are:
- @JsonProperty
- @JsonFormat
- @JsonUnwrapped
- @JsonView
- @JsonManagedReference and @JsonBackReference
- @JsonIdentityInfo
- @JsonFilter
@JsonProperty
The @JsonProperty annotation is used to map property names with JSON keys during serialization and deserialization. By default, if you try to serialize a POJO, the generated JSON will have keys mapped to the fields of the POJO. If you want to override this behavior, you can use the @JsonProperty annotation on the fields. It takes a String attribute that specifies the name that should be mapped to the field during serialization.
You can also use this annotation during deserialization when the property names of the JSON and the field names of the Java object do not match.
Let us consider an example Java class that uses the @JsonProperty annotation.
PropertyDemoBean.java
The test code to test the @JsonProperty annotation is:
The output of running the test in IntelliJ is:
@JsonFormat
The @JsonFormat annotation is used to tell Jackson that the format in which the value for a field is serialized. It specifies the format using the JsonFormat.Shape enum.
Let us consider an example Java class that uses the @JsonFormat annotation to modify the Date and Time format of an activeDate field.
FormatDemoBean.java
The test code to test the @JsonFormat annotation is:
The output of running the test in IntelliJ is:
@JsonUnwrapped
The @JsonUnwrapped annotation unwraps the values during serialization and deserialization. It helps in rendering the values of a composed class as if they belonged to the parent class. Let us consider an example of Java class that uses the @JsonUnwrapped annotation.
UnwrappedDemoBean.java
In this example, the Address class is inside the UnwrappedDemoBean class. Without the @JsonUnwrapped annotation, the serialized Java object would be similar to this:
Let us see what happens when you use the @JsonUnwrapped annotation.
The test code to test the @JsonUnwrapped annotation is:
The output of running the test in IntelliJ is:
As you can see, the Address object is unwrapped and is displayed as the properties of the parent classUnwrappedDemoBean.
@JsonView
The @JsonView annotation is used to include or exclude a property dynamically during serialization and deserialization. It tells the view in which the properties are rendered. Let us consider an example Java class that uses the @JsonView annotation with Public and Internal views.
ViewDemoBean.java
The test code to test the @JsonView annotation is:
As you can see in the test code, you need to configure the ObjectMapper to include which type of view must be used for writing the JSON from the Java object using the writerWithView() method.
The output of running the test in IntelliJ is:
When the JSON is generated in the public view, only personId and name fields are serialized omitting the gender field. But when the JSON is generated in the internal view all the fields are serialized.
@JsonManagedReference and @JsonBackReference
The @JsonManagedReference and @JsonBackReference annotation are used to create JSON structures that have a bidirectional relationship. Without this annotation, you get an error like this:
Let us consider an example Java class that uses the @JsonManagedReference and @JsonBackReference annotations:
ManagedReferenceDemoBean.java
BackReferenceDemoBean.java
The test code to test both @JsonManagedReference and @JsonBackReference annotations is:
The output of running the test in IntelliJ is:
As you can see, the field marked with @JsonManagedReference is the forward reference which will be included during serialization. The field marked with @JsonBackReference is the back reference and is usually omitted during serialization.
@JsonIdentityInfo
The @JsonIdentityInfo tells Jackson to perform serialization or deserialization using the identity of the object. This annotation works similar to the @JsonManagedReference and @JsonBackReference annotations with the difference that @JsonIdentityInfo includes the back reference object.
Let us consider an example where the IdentityInfoEmployeeDemoBean has a bidirectional relationship withIdentityInfoManagerDemoBean using the @JsonIdentityInfo annotation.
IdentityInfoEmployeeDemoBean.java
IdentityInfoManagerDemoBean.java
The test code to test the @JsonIdentityInfo annotation is:
The output of running the test in IntelliJ is:
As you can see, the output gives the information about the employee with his manager details. It also provides the additional information about the employees under the manager.
@JsonFilter
The @JsonFilter annotation is used to tell Jackson to use a custom defined filter to serialize the Java object. To define your filter, you need to use the FilterProvider class. This provider gets the actual filter instance to use. The filter is then configured by assigning the FilterProvider to ObjectMapper.
Let us consider an example of Java class that uses the @JsonFilter annotation.
FilterDemoBean.java
The test code to test the @JsonFilter annotation is:
The output of running the test in IntelliJ is:
As you can see, the custom filter declared as the arguments of the @JsonFilter annotation extract only the name and filters out the other properties of the bean during serialization.