Java and JSON – Jackson Serialization with ObjectMapper

This guide contains examples that show you how to serialize and deserialize from Java to JSON. It explains how to work with Strings, Objects, LocalDate, Optional, Lists, Maps, etc.

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!

Serialization

Jackson - Serialize String

@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.

Jackson - Serialize String to JSON Object

@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\"}"
  );
}

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, you introduce the field string. Note how simple it is:

  • 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 that you can use as you need.

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 Practical Software Architecture

Jackson - Serialize a List of String

@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 gets 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 below serializeListOfPersonName).

Jackson - Serialize a Map of String

@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 treats Maps as JSON objects by default. It serializes a Map as an object whose keys are its fields, so it calls the key's toString() method (in this case it's just the String). The Map values are serialized using the defaults unless you override them. 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.

Jackson - Serialize a String 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 indication in JSON of the class name, only its fields.

Jackson - Serialize objects with 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 be able 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 extensions available.

Jackson - Serialize objects with 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.

Deserialization

Jackson - 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 example below if you want to use explicit types.

Jackson - Deserialize and Unwrap Strings

@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 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 example, but it shows the basics in case you want to build a more complex deserializer.

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

Jackson - 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, findValuesAsText() 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 up and down, retrieve values using a path, etc. It's a convenient way of reading JSON contents without needing to create all the associated POJOs.

Jackson - Deserialize to 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 included before to deserialize the corresponding JSON contents, you'll get an exception. This is due to not having an empty constructor that Jackson can use during its deserialization logic (using reflection utils). 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 Practical Software Architecture

 Jackson - Deserialize to simple Java object with 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 when deserializing. In this case the example is using the default array format for LocalDate.

Jackson - Deserialize to 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 just annotate the parameters to the fields we want to parse with @JsonProperty.

Jackson - 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 the example proves, 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 next.

Jackson - Deserialize to 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.

Jackson - Deserialize to Java object ignoring unknown fields with Annotation

@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 have the ability to 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 including new fields for new versions. It can also help to implement Domain Driven Design when you're making shallow copies of your data in different system modules, so you can have classes with only the fields you're interested in, and safely ignore others.

Jackson - Deserialize to a Map

@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 Practical Software Architecture

Jackson - Deserialize to a List of Maps

@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.

Jackson - Deserialize to a List of 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.

Jackson - Deserialize Optional

@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.
Do you need help?

Amsterdam, The Netherlands https://thepracticaldeveloper.com