Dealing with Value Objects in REST API with Jackson

Dealing with serialization and deserialization of Value Objects with Jackson seems to be straightforward, right? It’s very common to expose REST API in our applications and JSON format is one of the most frequently used ones. How do we usually do it?

Data Transfer Objects

One of the most popular approaches is to introduce a set of Data Transfer Objects, which define external messages format. Let’s have a look at an example.

public class Project {

	private final ProjectId id;
	private final String name;

	public Project(ProjectId id, String name) {
		this.id = id;
		this.name = name;
	}

	public ProjectId getId() {
		return id;
	}

	public String getName() {
		return name;
	}
}

public class ProjectId {

	private final String id;

	public ProjectId(String id) {
		this.id = id;
	}

	public String getId() {
		return id;
	}
}

public class ProjectDTO {

	private String id;
	private String name;

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

This example is very simple, but even now we have to transform ProjectId to String during serialization and String to ProjectId during deserialization. What will happen when Value Object is more complex and contains 3-4 fields? and one of the fields is another Value Object?

public class Address {

	private final String streetName;
	private final Integer houseNumber;
	private final ZipCode zipCode;
	private final String city;
	
	public Address(String streetName, Integer houseNumber, ZipCode zipCode, String city) {
		this.streetName = streetName;
		this.houseNumber = houseNumber;
		this.zipCode = zipCode;
		this.city = city;
	}
	...
}

public class ZipCode {
	
	private final String code;

	public ZipCode(String code) {
		this.code = code;
	}
	...
}

In case of Address, dealing with serialization and deserialization Address to/from AddressDto is not a very pleasant task to do. It is not just a matter of adding AddressDto in each resource containing Address, but also transforming the values.

public class CustomerDto {

    private String name;
    private AddressDto address;
    // other fields omitted for brevity

}

public class AddressDto {

	private String streetName;
	private Integer houseNumber;
	private String zipCode;
	private String city;
	
	public Address toAddress() {
		ZipCode zipCodeValue = new ZipCode(zipCode);
		return new Address(streetName, houseNumber, zipCodeValue, city);
	}
	
	public AddressDto(Address address) {
		this.streetName = address.getStreetName();
		this.houseNumber = address.getHouseNumber();
		this.zipCode = address.getZipCode().getCode();
		this.city = address.getCity();
	}
}

Each time you want to compose Address in your Data Transfer Objects (such as CustomerDto in the example above) you have to prepare a data structure (AddressDto in this case) and the transformation. Of course, it can be reused in a single module, but not in case of a multi-module complex application — usually the DTO/converter code is duplicated.

There are tools to make the mapping and transformation easier, but anyway:

  1. You have to do it (in the code, tool configuration, etc.)
  2. You have to test it, because it is very probable that you will forget to pass some value.

Why can not we use the Value Object directly and serialize/deserialize its instances?

Please take a look at the full example of Address value object again. What will happen when you try to deserialize the object from JSON string?

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `Address` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"streetName":"5th Avenue","houseNumber":12,"zipCode":{"code":"34564"},"city":"New York"}"; line: 1, column: 2]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)

Jackson annotations

You can easily fix it by adding some Jackson annotations which allow defining a way how the object should be constructed:

public class Address {

	private final String streetName;
	private final Integer houseNumber;
	private final ZipCode zipCode;
	private final String city;
	
	@JsonCreator
	public Address(@JsonProperty(value = "streetName", required = true) String streetName, 
			@JsonProperty(value = "houseNumber", required = true) Integer houseNumber, 
			@JsonProperty(value = "zipCode", required = true) ZipCode zipCode, 
			@JsonProperty(value = "city", required = true) String city) {
		this.streetName = streetName;
		this.houseNumber = houseNumber;
		this.zipCode = zipCode;
		this.city = city;
	}

	...
}

Now Address can be deserialized from JSON and everything works fine but…

We have just added some additional dependency to Jackson, which sometimes may not be an acceptable approach. In case of a project where Address is used internally in a single module, it works perfectly fine and we can live with this Jackson dependency.

It becomes more of a challenge if we expose a set of interfaces and value objects to the external world (e.g. as a library). We should avoid forcing users to include Jackson in their project just because it was convenient for our internal use.

Is there any other way to deal with simple Value Objects?

Custom serializer / deserializer

I think we should deal with this situation in exactly the same way as we deal with Java 8 date/time classes. We can prepare set of custom serializers and deserializers.

At first glance, it looks like a non-trivial task to deal with low-level Jackson API. Actually, it is much simpler than you might think. The main benefit of this approach is that you code and test it once and then you can reuse it whenever you want.

public class AddressDeserializer extends StdDeserializer<Address> {

	@Override
	public Address deserialize(JsonParser jp, DeserializationContext ctx) throws IOException, JsonProcessingException {
		JsonNode rootNode = jp.getCodec().readTree(jp);

		String streetName = getNode(rootNode, "streetName")
				.filter(node -> node.isTextual())
				.map(node -> node.textValue())
				.orElseThrow(() -> new JsonMappingException(jp, "Missing textual 'streetName' property"));
		Integer houseNumber = getNode(rootNode, "houseNumber")
				.filter(node -> node.isInt())
				.map(node -> node.intValue())
				.orElseThrow(() -> new JsonMappingException(jp, "Missing int 'houseNumber' property"));
		String city = getNode(rootNode, "city")
				.filter(node -> node.isTextual())
				.map(node -> node.textValue())
				.orElseThrow(() -> new JsonMappingException(jp, "Missing textual 'city' property"));

		JsonNode zipCodeNode = getNode(rootNode, "zipCode")
				.orElseThrow(() -> new JsonMappingException(jp, "Missing 'zipCode' property"));
		ZipCode zipCode = zipCodeNode.traverse(jp.getCodec()).readValueAs(ZipCode.class);

		return new Address(streetName, houseNumber, zipCode, city);
	}

	private Optional<JsonNode> getNode(JsonNode node, String name) {
		return Optional.ofNullable(node.get(name));
	}

}

For simple Value Objects which are just wrappers for string values, we can prepare a common serializer and deserializer:

public class ValueObjectDeserializer<T> extends StdDeserializer<T> {

	private final Function<String, T> constructor;

	public ValueObjectDeserializer(Class vc, Function<String, T> constructor) {
		this.constructor = constructor;
	}

	@Override
	public T deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException, JsonProcessingException {
		JsonNode node = jsonParser.getCodec().readTree(jsonParser);

		String value = node.asText();

		return constructor.apply(value);
	}

}

public class ValueObjectSerializer<T> extends StdSerializer<T> {

	private final Function<T, String> valueExtractor;

	public ValueObjectSerializer(Class vc, Function<T, String> valueExtractor) {
		this.valueExtractor = valueExtractor;
	}

	@Override
	public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException {
		gen.writeString(valueExtractor.apply(value));
	}

}

The only thing we have to do is to register it properly in a custom Jackson module:

public class CustomModule extends Module {

        // non-relevant methods omitted for brevity

	@Override
	public void setupModule(SetupContext context) {
		context.addSerializers(createSerializers());
		context.addDeserializers(createDeserializers());
	}

	private Serializers createSerializers() {
		SimpleSerializers serializers = new SimpleSerializers();
		serializers.addSerializer(zipCodeSerializer());
		return serializers ;
	}

	private Deserializers createDeserializers() {
		SimpleDeserializers deserializers = new SimpleDeserializers();
		deserializers.addDeserializer(ZipCode.class, zipCodeDeserializer());
		return deserializers ;
	}

	private ValueObjectDeserializer zipCodeDeserializer() {
		return new ValueObjectDeserializer<>(ZipCode.class, value -> new ZipCode(value));
	}
	
	private ValueObjectSerializer zipCodeSerializer() {
		return new ValueObjectSerializer<>(ZipCode.class, value -> value.getCode());
	}

}

Comparison of approaches

Thus far, we considered three approaches to serialization of value objects:

  1. Mapping them to and from DTOs
  2. Annotating value objects directly
  3. Writing custom serializers and deserializers and delivering them in a Jackson module

I summarized pros and cons of different approaches in the table below.

 ProsCons
DTOs
  • easy and straightforward for simple classes
  • easy to check the JSON format just by looking at the source code
  • can cause duplication of data structures when Value Object is required to be used in more than one application or module

  • requires additional tests to check if serialization is correct in each module separately

  • you can write a common DTO/VO converter and share it with other modules, but the API users have to be aware of the conversion and explicitly use it whenever they want to serialize or deserialize messages
Jackson annotations directly on Value Objects
  • easy to use
  • allows to reuse mapping between internal modules
  • requires additional dependency to Jackson annotations – not acceptable when exposed to external systems
Custom serializer / deserializer
  • easy to reuse between modules – write & test once, use everywhere
  • easy to distribute as an additional Jackson module
  • requires low-level implementation (but it is much simpler than expected)
  • harder to define required/optional fields

 

Recommended scenarios

While a custom Jackson module is a great solution for some cases, there are cases where the traditional DTO-based model works perfectly fine. Based on the pros and cons listed above, I think that we can define the target scenario for each approach:

Recommended usage scenario
DTOsHaving Jackson annotations on value objects is not an option. Value Object is used in a single module – it’s easy to reuse data structures and transformations
Jackson annotations directly on Value ObjectsValue Objects are used in multiple modules internally and Jackson dependency is not a problem (e.g. when target module uses Jackson anyway)
Custom serializer / deserializerHaving Jackson annotations on value objects is not an option. Value Object is reused in many modules or external applications. It is worth to spend a bit more effort to prepare a serializer / deserializer once and reuse it

 

Summary

As we can see, a silver bullet solution does not exist, even for a problem as simple as value object serialization. There are consequences of each design decision we make. They are very important especially when there are external consumers of our classes. Adding an annotation on our class might seem like a good idea, but it can also mean that somebody has to include a Jackson jar in their project. It is always a worthwhile experience to assess our decisions by placing ourselves in our users’ shoes. I encourage you to do so on daily basis!

Related Post

2 response to "Dealing with Value Objects in REST API with Jackson"

  1. By: Stanislav Liesnychevskyi Posted: May 22, 2019

    Thank you very much for this article

Leave a Reply

Your email address will not be published. Required fields are marked *