JSON Serialization

JSON serialization #

A number of (external) libraries allow transforming Java objects into JSON objects and conversely. For instance Jackson, Gson, JSON-java, JSON-B and JSON-P.

In this section, we focus on Jackson.

Note. Jackson was initially designed for JSON, but extensions of Jackson allow manipulating other formats: XML, TOML, YAML, CSV and Java property files.

Install #

Jackson can be used within a Maven project, by declaring the following dependency

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.17.0</version>
</dependency>

And similarly for Gradle:

implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: 2.17.0

For the latest version, search Maven Central.

Installing this dependency will transitively install:

Basic syntax #

Many tutorials can be found online about Jackson: for instance Jackson in N minutes on the GitHub page of the jackson-databind library.

We focus here on some simple features of the library.

An ObjectMapper can be used to map a Java object (or array or value) to some JSON string, and conversely.

ObjectMapper mapper = new ObjectMapper();
// Optional: indent the output JSON strings (and adds line breaks) when applicable
mapper.enable(SerializationFeature.INDENT_OUTPUT);

Convert a value #

Serialize #

// Outputs 15
System.out.println(mapper.writeValueAsString(15));

// Outputs "abcd"
System.out.println(mapper.writeValueAsString("abcd"));

Deserialize #

// Creates an Integer with value 15
Integer integer = mapper.readValue("15", Integer.class);

Convert an array or collection #

Serialize #


// Outputs [2,3]
System.out.println(mapper.writeValueAsString(new int[]{2, 3}));

// Outputs either [2,3] or [3,2]
System.out.println(mapper.writeValueAsString(Set.of(2, 3)));

Map<String, Integer> studentToAge = Map.of(
  "Alice", 20,
  "Bob", 19
);

/* Outputs
  {
    "Alice" : 20,
    "Bob" : 19
  }
*/
System.out.println(mapper.writeValueAsString(studentToAge));

Map<String, List<Integer>> studentToMarks = Map.of(
    "Alice", List.of(8,9),
    "Bob", List.of(6,10)
);

/* Outputs
  {
    "Alice" : [ 8, 9 ],
    "Bob" : [ 6, 10 ]
  }
*/
System.out.println(mapper.writeValueAsString(studentToMarks));

Deserialize #

// Creates an array with values [2,3,2]
int[] integers = mapper.readValue("[2,3,2]", int[].class);

// Creates an Arraylist with values [2,3,2]
List<Integer> list = mapper.readValue("[2,3,2]", List.class);

// Creates a HashSet with values {2,3}
Set<Integer> set = mapper.readValue("[2,3,2]", Set.class);

// Creates a LinkedHashMap with values {"Alice" -> 20, "Bob" -> 19}
Map<String, Integer> map = mapper.readValue("{\"Alice\": 20, \"Bob\": 19}", Map.class);

Convert an object #

Serialize #

public class City {

    public String name;
    public Country country;

    public City(String name, Country country) {
        this.name = name;
        this.country = country;
    }
}
public class Country {

    public String name;
    public City capital;

    public Country(String name, City capital) {
        this.name = name;
        this.capital = capital;
    }
}
Country italy = new Country("Italy", null);
City rome = new City("Rome", italy);

/* Outputs:
  {
      "name" : "Rome",
      "country" : {
          "name" : "Italy",
          "capital" : null
  }
*/
System.out.println(mapper.writeValueAsString(rome));

However, recall that a JSON object cannot contain a reference to another JSON object. As a consequence, some Java objects cannot be finitely represented in JSON:

italy.capital = rome;
// Throws a JsonMappingException: Document nesting depth (1001) exceeds the maximum allowed
mapper.writeValueAsString(rome);

By default, Jackson serializes:

  • public (instance) attributes,
  • non-public fields that have a getter method.

However, it is also possible to force serialization of other attributes, either for all classes:

mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

or for a specific class:

@JsonAutoDetect(fieldVisibility = Visibility.ANY)
public class MyClass {
    ...
}

Warning. If a public attribute attribute and a getter getAttribute coexist, then the latter takes precedence.

Some public attributes can also be explicitly ignored:

@JsonIgnoreProperties("name")
public class MyClass {
    public String name;
    ...
}

Deserialize #

In order to create a Java object out of a JSON object, the attributes (a.k.a. “keys”) of the JSON object must be mapped to attributes of the targeted class.

This can be done by adding (Jackson-specific) annotations to a constructor for the class. For instance:

public class City {

    public String name;
    public int zipCode;

    @JsonCreator
    public City(@JsonProperty("name") String name, @JsonProperty("code") int zipCode) {
        this.name = name;
        this.zipCode = zipCode;
    }
}
String jsonCity = "{\"name\" : \"Bologna\", \"code\" : 40100 }";

// Contains the object City{ name: "Bologna", zipCode: 40100 }
City bologna = mapper.readValue(jsonCity, City.class);

In the example above, the annotations @JsonProperty("name") and @JsonProperty("code") indicate which JSON attributes are used to create an instance of Country. Note that the name of the JSON attribute and the name of the constructor argument can differ (e.g. “code” and zipCode in this example).

These annotations can also be used to populate more complex structures, such as nested objects, arrays, collections, etc. For instance:

public class Country {

    String name;

    @JsonCreator
    public Country(@JsonProperty("name") String name) {
      this.name = name;
    }
}
String jsonCountries = "[ " +
    "{\"name\" : \"Italy\"}, " +
    "{\"name\" : \"Austria\"} " +
  "]";

/* Contains the objects
      Country{ name: "Italy" }
 and
      Country{ name: "Austria" },
 in this order.
*/
Country[] countries = mapper.readValue(jsonCountries, Country[].class);

What does the following program print?

public class City {

    public String name;
    public Country country;

    @JsonCreator
    public City(@JsonProperty("name") String name, @JsonProperty("country") Country country) {
        this.name = name;
        this.country = country;
    }
}
public class Country {

    public String name;

    @JsonCreator
    public Country(@JsonProperty("name") String name){
        this.name = name;
    }
}
Country italy = new Country("Italy");

City rome = new City("Rome", italy);
City bologna = new City("Bologna", italy);
City[] cities = new City[]{rome, bologna};
// count countries
System.out.println(countCountries(cities));

// serialize
String serializedCities = mapper.writeValueAsString(cities);
// deserialize
City[] deserializedCities = mapper.readValue(serializedCities, City[].class);
// count countries
System.out.println(countCountries(deserializedCities));

// Counts the number of distinct countries that appear in the input array
private int countCountries(City[] cities) {
    Set<Country> countries = new HashSet<>();
    for(City city: cities){
        countries.add(city.country);
    }
    return countries.size();
}

1

2

Writing to or reading from a file #

The ObjectMapper class provides utility methods to write (resp. read) the JSON output (resp. input) directly to (resp. from) a file. In particular, in the examples above:

  • writeValueAsString(o) can be replaced with writeValue(file, o),
  • readValue(string, class) can be replaced with readValue(file, class).

For instance:

// Serialize
try {
      mapper.writeValue(new File("path/to/file.json"), rome);
} catch (IOException e) {
      throw new RuntimeException(e);
}
// Deserialize
City myCity;
try {
      myCity = mapper.readValue(new File("path/to/file.json"), City.class);
} catch (IOException e) {
      throw new RuntimeException(e);
}