Jackson Datatype Module for Result

Result-Jackson provides a Jackson datatype module for Result objects

Result Jackson Module

This library provides a Jackson datatype module for Results objects.

Introduction

When using Result objects with Jackson we might run into some problems. This library solves them by making Jackson treat results as if they were ordinary objects.

Let’s start by creating a class ApiResponse containing one ordinary and one Result field:

/** Represents an API response */
public class ApiResponse {

  @JsonProperty
  String version;

  @JsonProperty
  Result<String, String> result;

  // Constructors, getters and setters omitted
}

Problem Overview

We will take a look at what happens when we try to serialize and deserialize ApiResponse objects with Jackson.

First, let’s make sure we’re using the latest versions of both libraries, Jackson and Result.

Serialization Problem

Now, let’s instantiate an ApiResponse object:

/*  Create new response */
ApiResponse response = new ApiResponse();
response.setVersion("v1"); // Set version
response.setResult(success("Perfect")); // Set result

And finally, let’s try serializing it using an object mapper:

/*  Serialize the response object */
ObjectMapper objectMapper = new ObjectMapper(); // Create new object mapper
String json = objectMapper.writeValueAsString(response); // Serialize as JSON

We’ll see that now we get an InvalidDefinitionException:

Java 8 optional type `java.util.Optional<java.lang.String>`
 not supported by default:
 add Module "com.fasterxml.jackson.datatype:jackson-datatype-jdk8"
 to enable handling

Although this may look strange, it’s actually what we should expect. In this case, getSuccess() is a public getter on the Result interface that returns an Optional<String> value, which is not supported by Jackson unless you have registered the modules that deal with JDK 8 datatypes.

/** Test serialization problem */
@Test
void serialization_problem() throws Exception {
  // Given
  ApiResponse response = new ApiResponse("v1", success("Perfect"));
  // Then
  ObjectMapper objectMapper = new ObjectMapper();
  InvalidDefinitionException error = assertThrows(InvalidDefinitionException.class,
      () -> objectMapper.writeValueAsString(response));
  assertTrue(error.getMessage().startsWith(
      "Java 8 optional type `java.util.Optional<java.lang.String>` not supported"));
} // End

This is Jackson’s default serialization behavior. But we’d like to serialize the result field like this:

{
  "version": "v1",
  "result": {
    "failure": null,
    "success": "Perfect"
  }
}

Deserialization Problem

Now, let’s reverse our previous example, this time trying to deserialize a JSON object into an ApiResponse:

/*  Deserialize a JSON string */
String json = "{\"version\":\"v2\",\"result\":{\"success\":\"OK\"}}";
ObjectMapper objectMapper = new ObjectMapper(); // Create new object mapper
objectMapper.readValue(json, ApiResponse.class); // Deserialize the response

We’ll see that we get another InvalidDefinitionException. Let’s view the stack trace:

Cannot construct instance of `com.leakyabstractions.result.api.Result`
 (no Creators, like default constructor, exist):
 abstract types either need to be mapped to concrete types,
 have custom deserializer, or contain additional type information

This behavior again makes sense. Essentially, Jackson doesn’t have a clue how to create new Result objects, because Result is just an interface, not a concrete type.

/** Test deserialization problem */
@Test
void deserialization_problem() {
  // Given
  String json = "{\"version\":\"v2\",\"result\":{\"success\":\"OK\"}}";
  // Then
  ObjectMapper objectMapper = new ObjectMapper();
  InvalidDefinitionException error = assertThrows(InvalidDefinitionException.class,
      () -> objectMapper.readValue(json, ApiResponse.class));
  assertTrue(error.getMessage().startsWith(
      "Cannot construct instance of `com.leakyabstractions.result.api.Result`"));
} // End

Solution

What we want, is for Jackson to treat Result values as JSON objects that contain either a success or a failure value. Fortunately, this problem has been solved for us. This library provides a Jackson module that deals with Result objects.

First, let’s add the latest version as a Maven dependency:

All we need to do now is register ResultModule with our object mapper:

/*  Register ResultModule */
ObjectMapper objectMapper = new ObjectMapper(); // Create new object mapper
objectMapper.registerModule(new ResultModule()); // Register manually

Alternatively, you can also auto-discover the module with:

/*  Register ResultModule */
objectMapper.findAndRegisterModules(); // Register automatically

Regardless of the registration mechanism used, once the module is registered all functionality is available for all normal Jackson operations.

Serialization Solution

Now, let’s try and serialize our ApiResponse object again:

/** Test serialization solution with a successful result */
@Test
void serialization_solution_successful_result() throws Exception {
  // Given
  ApiResponse response = new ApiResponse("v3", success("All good"));
  // When
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.registerModule(new ResultModule());
  String json = objectMapper.writeValueAsString(response);
  // Then
  assertTrue(json.contains("v3"));
  assertTrue(json.contains("All good"));
} // End

If we look at the serialized response, we’ll see that this time the result field contains a null failure value and a non-null success value:

{
  "version": "v3",
  "result": {
    "failure": null,
    "success": "All good"
  }
}

Next, we can try serializing a failed result:

/** Test serialization problem with a failed result */
@Test
void serialization_solution_failed_result() throws Exception {
  // Given
  ApiResponse response = new ApiResponse("v4", failure("Oops"));
  // When
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.findAndRegisterModules();
  String json = objectMapper.writeValueAsString(response);
  // Then
  assertTrue(json.contains("v4"));
  assertTrue(json.contains("Oops"));
} // End

And we can verify that the serialized response contains a non-null failure value and a null success value:

{
  "version": "v4",
  "result": {
    "failure": "Oops",
    "success": null
  }
}

Deserialization Solution

Now, let’s repeat our tests for deserialization. If we read our ApiResponse again, we’ll see that we no longer get an InvalidDefinitionException:

/** Test deserialization solution with a successful result */
@Test
void deserialization_solution_successful_result() throws Exception {
  // Given
  String json = "{\"version\":\"v5\",\"result\":{\"success\":\"Yay\"}}";
  // When
  ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
  ApiResponse response = objectMapper.readValue(json, ApiResponse.class);
  // Then
  assertEquals("v5", response.getVersion());
  assertEquals("Yay", response.getResult().orElse(null));
} // End

Finally, let’s repeat the test again, this time with a failed result. We’ll see that yet again we don’t get an exception, and in fact, have a failed Result:

/** Test deserialization solution with a failed result */
@Test
void deserialization_solution_failed_result() throws Exception {
  // Given
  String json = "{\"version\":\"v6\",\"result\":{\"failure\":\"Nay\"}}";
  // When
  ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
  ApiResponse response = objectMapper.readValue(json, ApiResponse.class);
  // Then
  assertEquals("v6", response.getVersion());
  assertEquals("Nay", response.getResult().getFailure().orElse(null));
} // End

Conclusion

We’ve shown how to use Result with Jackson without any problems by leveraging the Jackson datatype module for Result, demonstrating how it enables Jackson to treat Result objects as ordinary fields.

The implementation of these examples can be found here.