Java and JSON – Jackson Serialization with ObjectMapper

This guide contains examples that show you how to serialize and deserialize from Java to JSON. It covers the basics and the most common use cases: Strings, Objects, Dates, Optionals, Lists, Maps, etc.

Table of Contents

The code blocks are just test cases so you can verify the output and play with the examples. Note that, in real life, you shouldn't create the `ObjectMapper` each time you need to parse or generate JSON. It's much more efficient to reuse it once you have configured it. This is, for instance, what you do in Spring with dependency injection.

All the code in this post is available on GitHub: Jackson Serialization Examples. If you find it useful, please give it a star!

JSON Serialization with Java

Serialize plain Strings

@Test
public void serializeSimpleString() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  var personName = "Juan Garcia";
  var json = mapper.writeValueAsString(personName);
  log.info("Serializing a plain String: {}", json);
  assertThat(json).isEqualTo("\"Juan Garcia\"");
}

It's a very simple case, yet confusing sometimes. A plain String does not get mapped magically to a JSON object (that is, between curly brackets and with a field name). When you send this via HTTP, Kafka or AMQP, just to mention some examples, it may happen that the consumer is expecting an object so it fails at deserialization. It's a common mistake when developing Spring Controllers that return a String. In that case, you can create simple objects whose only intention is to wrap a String, or you can code your own serializers or deserializers.

Serialize Strings as JSON Objects

@Test
public void serializeStringAsObject() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  var stringWrapperSerializer = new StdSerializer<String>(String.class) {
    @Override
    public void serialize(String s,
                          JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider)
      throws IOException {
      jsonGenerator.writeStartObject();
      jsonGenerator.writeStringField("string", s);
      jsonGenerator.writeEndObject();
    }
  };
  mapper.registerModule(new SimpleModule()
    .addSerializer(stringWrapperSerializer));

  var personName = "Juan Garcia";
  var json = mapper.writeValueAsString(personName);
  log.info("Using a custom serializer (in this case for a String): {}", json);
  assertThat(json).isEqualTo(
    "{\"string\":\"Juan Garcia\"}"
  );
}

As shown above, you can create your own Serializer for the String class, and configure it to create a basic JSON object that wraps the string value. In this case, the example uses a field with a key named string. This is how it works:

  • You use the default implementation StdSerializer and override the serialize method.
  • Then, you use another default implementation, in this case for a module: SimpleModule, and you just add the new serializer to it.
  • You only need to register your module in the ObjectMapper and all the String objects serialized by that mapper will be converted to JSON objects.

Remember that you should configure your ObjectMapper objects in advance and then reuse them in your application. As you can imagine, you can have different instances, each one with its own configuration.

As shown above, you can add custom behavior by registering modules in your ObjectMapper. Most of the examples in this guide use this feature to provide custom configuration.

Get the book: Full Reactive Stack with WebFlux, MongoDB and Angular

Serialize a List of String objects

@Test
public void serializeListOfString() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  var personNames = List.of("Juan Garcia", "Manuel Perez");
  var json = mapper.writeValueAsString(personNames);
  log.info("A simple list of String objects looks like this: {}", json);
  assertThat(json).isEqualTo(
    "[\"Juan Garcia\",\"Manuel Perez\"]"
  );
}

A list of String objects is serialized by default as a simple JSON array. You could use a custom wrapper like the one from the previous example to get an array of JSON objects instead. As an alternative, you can also create your own class with a single field to act as a wrapper (see the method serializeListOfPersonName) in the "Serialize a String with a wrapper class".

Serialize a Map of <String,String> key-values

@Test
public void serializeMapOfString() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  var personNames = new TreeMap<String, String>();
  personNames.put("name1", "Juan Garcia");
  personNames.put("name2", "Manuel Perez");
  var json = mapper.writeValueAsString(personNames);
  log.info("A simple map of <String, String>: {}", json);
  assertThat(json).isEqualTo(
    "{\"name1\":\"Juan Garcia\",\"name2\":\"Manuel Perez\"}"
  );
}

Jackson serializes a Map as a JSON object whose keys are the keys’ toString() representation (in this case it’s the value itself). The Map values are serialized using the default serializers unless you override them. That means you can also set Maps as values if you want nested JSON objects, or you can use Java objects that are serialized with the per-field strategy as usual.

Serialize a String with a Wrapper class

public class PersonName {
  private final String name;

  public PersonName(String name) {
    this.name = name;
  }

  // Alternatively, you can use a public field so you don't need a getter
  public String getName() {
    return name;
  }
}
@Test
public void serializeListOfPersonName() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  var personNames = List.of(
    new PersonName("Juan Garcia"),
    new PersonName("Manuel Perez")
  );
  var json = mapper.writeValueAsString(personNames);
  log.info("A list of simple PersonName objects converted to JSON: {}", json);
  assertThat(json).isEqualTo(
    "[{\"name\":\"Juan Garcia\"},{\"name\":\"Manuel Perez\"}]"
  );
}

This is the first example serializing a Java object. The default serializer takes all the public fields and all the fields that are accessible via getters. You can alter this behavior with annotations or custom serializers. In this example, PersonName is used as a wrapper for a simple String. A list of Java objects gets serialized to a list of JSON objects containing the fields and their values. Note that there is no reference in JSON to the class name, only its fields.

Serialize objects with a LocalDate (default)

public class Person {
  private final String name;
  private final LocalDate birthdate;

  public Person(String name, LocalDate birthdate) {
    this.name = name;
    this.birthdate = birthdate;
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }
}
@Test
public void serializeListOfPerson() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  var personNames = List.of(
    new Person("Juan Garcia",
      LocalDate.of(1980, 9, 15)),
    new Person("Manuel Perez",
      LocalDate.of(1987, 7, 23))
  );
  var json = mapper.writeValueAsString(personNames);
  log.info("A list of simple Person objects converted to JSON: {}", json);
  // By default, Jackson serializes LocalDate and LocalDateTime exporting
  // all the object fields as with any other object. You need to use the
  // JavaTimeModule, although the default formatter is a bit weird (array).
  assertThat(json).isEqualTo(
    "[{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]}," +
      "{\"name\":\"Manuel Perez\",\"birthdate\":[1987,7,23]}]"
  );
}

This example uses a more realistic Java Class, in this case using a LocalDate field (introduced in Java 8). To serialize those objects in a meaningful way, you need to register the JavaTimeModule, contained in the dependency library jackson-datatype-jsr310. It serializes the fields as an array by default. You could also avoid including that extra dependency if you use your custom serializers and deserializers, but that doesn't make much sense when there are available extensions.

Serialize objects with a LocalDate in ISO format

@Test
public void serializeListOfPersonFormatted() throws JsonProcessingException {
  var mapper = new ObjectMapper();
  // You can use a custom module to change the formatter
  mapper.registerModule(new SimpleModule().addSerializer(
    new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)));
  var personNames = List.of(
    new Person("Juan Garcia",
      LocalDate.of(1980, 9, 15)),
    new Person("Manuel Perez",
      LocalDate.of(1987, 7, 23))
  );
  var json = mapper.writeValueAsString(personNames);
  log.info("A list of simple Person objects converted to JSON: {}", json);
  // In this case you get the ISO format YYYY-MM-DD
  assertThat(json).isEqualTo(
    "[{\"name\":\"Juan Garcia\",\"birthdate\":\"1980-09-15\"}," +
      "{\"name\":\"Manuel Perez\",\"birthdate\":\"1987-07-23\"}]"
  );
}

Configuring the format of the parsed LocalDate is possible in multiple ways. In this case, you can use your own module instead of the JavaTimeModule, and add the LocalDateSerializer manually. This way you can set the format to, for instance, an ISO standard. The result is a date in YYYY-MM-DD format.

JSON Deserialization with Java

Deserialize List of Strings

@Test
public void deserializeListOfString() throws IOException {
  var mapper = new ObjectMapper();
  var json = "[\"Juan Garcia\",\"Manuel Perez\"]";
  var list = mapper.readValue(json, List.class);
  log.info("Deserializing a list of plain Strings: {}", list);
  assertThat(list).containsExactly("Juan Garcia", "Manuel Perez");
}

You can parse a JSON array of Strings to a list in a very straightforward manner. Note that in the example the list is not typed, see the next example if you want to use explicit types.

Deserialize wrapped Strings as a list of String objects

@Test
public void deserializeToListOfStringUsingCustomModule() throws IOException {
  var mapper = new ObjectMapper();

  var deserializer = new StdDeserializer<String>(String.class) {
    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt)
      throws IOException {
      return ((TextNode) p.getCodec().readTree(p).get("string")).textValue();
    }
  };

  mapper.registerModule(
    new SimpleModule().addDeserializer(String.class, deserializer));

  var json = "[{\"string\":\"Juan Garcia\"},{\"string\":\"Manuel Perez\"}]";
  // You can use this option or you can deserialize to String[]
  var stringCollectionType = mapper.getTypeFactory()
    .constructCollectionType(List.class, String.class);
  List<String> values = mapper.readValue(json, stringCollectionType);
  log.info("Using a custom deserializer to extract field values: {}", values);
  assertThat(values).containsExactly("Juan Garcia", "Manuel Perez");
}

One of the serializing examples in this guide showed how to wrap String values into JSON objects on-the-fly. This is the implementation of the deserializer for that specific case: you can implement your own deserializer and use the parser to read the JSON tree and extract values. This is just a simple case, but you can build a more complex deserializer using the same approach.

Note that this example also demonstrates how to instruct ObjectMapper to deserialize a typed List. You just need to create a CollectionType object and pass it when parsing the JSON contents.

Deserialize values using JsonNode

@Test
public void deserializeListOfStringObjectsUsingTree() throws IOException {
  var mapper = new ObjectMapper();
  var json = "[{\"string\":\"Juan Garcia\"},{\"string\":\"Manuel Perez\"}]";
  var values = mapper.readTree(json).findValuesAsText("string");
  log.info("Using the JSON tree reader to extract field values: {}", values);
  assertThat(values).containsExactly("Juan Garcia", "Manuel Perez");
}

This code does the same as the previous one, but it uses the ObjectMapper's readTree() method to get a JsonNode object and then use its methods to parse the values. In this case, the findValuesAsText() method is all you need to extract all the values matching that field name to a list.

There are some other useful methods in that class to traverse the JSON, retrieve values using a path, etc. It's a convenient way of reading JSON contents without needing to create all the corresponding Java classes.

Deserialize to a simple Java Object

@Test
public void deserializeListOfPersonDoesNotWork() {
  var mapper = new ObjectMapper();
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]}";
  var throwable = catchThrowable(() -> mapper.readValue(json, Person.class));
  log.info("You need to use an empty constructor: {}", throwable.getMessage());
  assertThat(throwable).isInstanceOf(InvalidDefinitionException.class);
}

If you try to use the Person class shown before to deserialize the corresponding JSON contents, you'll get an error. This is because we don't have an empty constructor that Jackson can use during its deserialization logic (using reflection). There are two common ways of making a plain Java object candidate for JSON deserialization: including an empty constructor or using Jackson annotations. The next two examples cover those scenarios.

Get the book: Full Reactive Stack with WebFlux, MongoDB and Angular

Deserialize to a simple Java object with an Empty Constructor

public class PersonEC {
  private String name;
  private LocalDate birthdate;

  public PersonEC() {
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }

  @Override
  public String toString() {
    return "PersonEC{" +
      "name='" + name + '\'' +
      ", birthdate=" + birthdate +
      '}';
  }
}
@Test
public void deserializeListOfPersonEmptyConstructor() throws IOException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]}";
  var value = mapper.readValue(json, PersonEC.class);
  log.info("Deserializing a simple POJO with empty constructor: {}", value);
  assertThat(value.getName()).isEqualTo("Juan Garcia");
  assertThat(value.getBirthdate())
    .isEqualTo(LocalDate.of(1980, 9, 15));
}

You can create a separate class as per the example or you can modify the existing one. Now you instruct the ObjectMapper to deserialize using the class with the empty constructor and everything should work as expected.

Note that you can use the JavaTimeModule also at deserialization time. In this case the example is using the default array format for LocalDate.

Deserialize to a simple Java object using Annotations

public class PersonAnnotated {
  private final String name;
  private final LocalDate birthdate;

  @JsonCreator
  public PersonAnnotated(
    @JsonProperty("name") String name,
    @JsonProperty("birthdate") LocalDate birthdate) {
    this.name = name;
    this.birthdate = birthdate;
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }
}
@Test
public void deserializePersonAnnotated() throws IOException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]}";
  var value = mapper.readValue(json, PersonAnnotated.class);
  log.info("Deserializing a simple POJO with @JsonCreator: {}", value);
  assertThat(value.getName()).isEqualTo("Juan Garcia");
  assertThat(value.getBirthdate())
    .isEqualTo(LocalDate.of(1980, 9, 15));
}

A popular way of using Jackson is by leveraging the annotations included in the jackson-annotations library. As you can see, we can annotate the constructor that should be used to create the object (@JsonCreator) and we also annotate the parameters with the field names, using @JsonProperty.

Deserialize to a different Java class

public class PersonV2 {
  private final String name;

  private final LocalDate birthdate;
  private final List<String> hobbies;

  @JsonCreator
  public PersonV2(@JsonProperty("name") String name,
                  @JsonProperty("birthdate") LocalDate birthdate,
                  @JsonProperty("hobbies") List<String> hobbies) {
    this.name = name;
    this.birthdate = birthdate;
    this.hobbies = hobbies;
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }

  public List<String> getHobbies() {
    return hobbies;
  }
}
@Test
public void deserializePersonV2AsPersonFails() {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}";
  var throwable = catchThrowable(() -> mapper.readValue(json, PersonEC.class));
  log.info("Trying to deserialize with unknown properties" +
    " fails by default: {}", throwable.getMessage());
  assertThat(throwable).isInstanceOf(UnrecognizedPropertyException.class);
}

This sample code is trying to deserialize a JSON object with an extra field that does not exist in PersonEC, hobbies. This field actually belongs to a serialized PersonV2 object. As this example shows, you can’t get backward compatibility by default in this case since the unknown property causes an error. But this has a very easy fix, as you’ll see in the next section.

Deserialize to a Java object ignoring unknown fields with configuration

@Test
public void deserializePersonV2AsPersonIgnoringProperties() throws IOException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}";
  PersonEC value = mapper.readValue(json, PersonEC.class);
  log.info("Deserializing a simple POJO ignoring unknown properties: {}", value);
  assertThat(value.getName()).isEqualTo("Juan Garcia");
  assertThat(value.getBirthdate())
    .isEqualTo(LocalDate.of(1980, 9, 15));
}

You can configure the ObjectMapper to ignore unknown properties as in this example, setting the value of DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES to false. As a result, the property hobbies is ignored since there is no field with such name in the class PersonEC. With this configuration, all the unknown properties for all objects deserialized with this mapper will be ignored.

Deserialize to a Java object ignoring unknown fields with annotations

@JsonIgnoreProperties(ignoreUnknown = true)
public class PersonAnnotated {
  // ...
}
@Test
public void deserializePersonV2AsPersonAnnotated() throws IOException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}";
  var value = mapper.readValue(json, PersonAnnotated.class);
  log.info("Using @JsonIgnoreProperties to ignore unknown fields: {}", value);
  assertThat(value.getName()).isEqualTo("Juan Garcia");
  assertThat(value.getBirthdate())
    .isEqualTo(LocalDate.of(1980, 9, 15));
}

You can also achieve the same result as in the previous example by annotating the classes that are the target of the deserialization with @JsonIgnoreProperties. This way you can choose for which classes you want to be flexible (or strict, in case you change also the default configuration).

Ignoring JSON fields can be risky but it's also a way to implement backward compatibility when classes are only adding new fields in new versions. It can also help to implement Domain-Driven Design if you're making shallow copies of your data in different system modules. Then, you can have classes containing only the fields you're interested in, and safely ignore others.

Deserialize a Java object to a Map of key-values

@Test
public void deserializePersonV2AsMap() throws IOException {
  var mapper = new ObjectMapper();
  var json = "{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}";
  Map value = mapper.readValue(json, Map.class);
  log.info("Deserializing a JSON object as a map: {}", value);
  assertThat(value.get("name")).isEqualTo("Juan Garcia");
  assertThat(value.get("birthdate"))
    .isEqualTo(Lists.newArrayList(1980, 9, 15));
  assertThat(value.get("hobbies"))
    .isEqualTo(Lists.newArrayList("football", "squash"));
}

Sometimes, it might be convenient to deserialize Java objects as simple types. In these cases, you can instruct Jackson to deserialize the object to a Map. It will contain field names as keys and their deserialized contents as values.

Note that the default deserializers apply for basic field types in the Map. Jackson deserializes the date as a collection, same as the array of hobbies.

Get the book: Full Reactive Stack with WebFlux, MongoDB and Angular

Deserialize a list of Java objects to a List of Map objects

@Test
public void deserializeListOfPersonV2AsListOfMaps() throws IOException {
  var mapper = new ObjectMapper();
  var json = "[{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}," +
    "{\"name\":\"Manuel Perez\",\"birthdate\":\"1987-07-23\"}]";
  // This is not needed since it's the default behavior (deserialize to map)
  var mapCollectionType = mapper.getTypeFactory()
    .constructCollectionType(List.class, Map.class);
  // You can also use List.class instead of mapCollectionType
  List<Map> value = mapper.readValue(json, mapCollectionType);
  log.info("Deserializing objects as a list of maps: {}", value);
  assertThat(value.get(0).get("name")).isEqualTo("Juan Garcia");
  assertThat(value.get(1).get("name")).isEqualTo("Manuel Perez");
}

If you want to apply the previous approach to a collection of objects, you can use the already-covered CollectionType to specify the type of collection you want to get as a result. But in this case, you don't need it. The default behavior of the ObjectMapper when deserializing Objects in a List is to create a list of field-value maps.

Deserialize a list of Java objects

@Test
public void deserializeListOfPersonV2AsListOfObjects() throws IOException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  var mapCollectionType = mapper.getTypeFactory()
    .constructCollectionType(List.class, PersonV2.class);
  var json = "[{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}," +
    "{\"name\":\"Manuel Perez\",\"birthdate\":\"1987-07-23\"}]";
  List<PersonV2> value = mapper.readValue(json, mapCollectionType);
  log.info("Deserializing to a list of POJOs: {}", value);
  assertThat(value.get(0).getName()).isEqualTo("Juan Garcia");
  assertThat(value.get(1).getName()).isEqualTo("Manuel Perez");
  assertThat(value.get(1).getHobbies()).isNull();
}

In this case, you want to explicitly set the type of the collection to PersonV2, otherwise you will get a Map as shown before. Remember that this strategy for passing types is needed because of Type Erasure, which prevents you from using generics as in List<PersonV2> value = mapper.readValue(json, List<PersonV2>.class);.

Note that the object without the hobbies field gets deserialized properly, and its value is set to null. You don't need to ignore the property in this case since the field is not present in JSON.

Deserialize Java Optionals

@Test
public void deserializeListOfPersonV2AsListUsingOptional() throws IOException {
  var mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  mapper.registerModule(new Jdk8Module());
  var mapCollectionType = mapper.getTypeFactory()
    .constructCollectionType(List.class, PersonV2Optional.class);
  var json = "[{\"name\":\"Juan Garcia\",\"birthdate\":[1980,9,15]," +
    "\"hobbies\":[\"football\",\"squash\"]}," +
    "{\"name\":\"Manuel Perez\",\"birthdate\":\"1987-07-23\"}]";
  List<PersonV2Optional> value = mapper.readValue(json, mapCollectionType);
  log.info("Deserializing to a list of POJOs using Optional values: {}", value);
  assertThat(value.get(0).getHobbies())
    .isPresent()
    .hasValueSatisfying(hobbies -> assertThat(hobbies).hasSize(2));
  assertThat(value.get(1).getHobbies()).isEmpty();
}

A more convenient way of dealing with not present fields is to make them Optional in your POJOs. That allows you to use its powerful API to set default values, execute conditional code, etc. This is the same example as before just with a modified version of PersonV2 that contains hobbies as an Optional<List>.

Jackson can be smart to transform not present values to empty Optionals instead of nulls. However, for this to work properly, you need to include the dependency jackson-datatype-jdk8 and configure the ObjectMapper registering the Jdk8Module.

Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Amsterdam, The Netherlands https://thepracticaldeveloper.com

Comments