Jackson 3.x Datatype Module for Result

Result-Jackson3 provides a Jackson 3.x datatype module for Result objects

Result Jackson 3.x Module

When using Result objects with Jackson we might run into some problems. The Jackson 3.x datatype module for Result solves them by making Jackson treat results as if they were ordinary objects.

Jackson is a Java library for JSON parsing and generation. It is widely used for converting Java objects to JSON and vice versa, making it essential for handling data in web services and RESTful APIs.

How to Use this Add-On

Add this Maven dependency to your build:

Maven Central provides snippets for different build tools to declare this dependency.

Test Scenario

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

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

  @JsonProperty
  String version;

  @JsonProperty
  Result<String, String> result;

  // Constructors, getters and setters omitted
}

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

Serializing Results

Next, let’s try serializing it using an object mapper.

/** Test serialization solution with a successful result */
@Test
void serializeSuccessfulResult() {
  // Given
  ApiResponse response = new ApiResponse("v1", success("All good"));
  // When
  ObjectMapper objectMapper = JsonMapper.builder().build();
  String json = objectMapper.writeValueAsString(response);
  // Then
  assertTrue(json.contains("v1"));
  assertTrue(json.contains("All good"));
} // End

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

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

Next, we can try serializing a failed result.

/** Test serialization problem with a failed result */
@Test
void serializeFailedResult() {
  // Given
  ApiResponse response = new ApiResponse("v2", failure("Oops"));
  // When
  ObjectMapper objectMapper = JsonMapper.builder().build();
  String json = objectMapper.writeValueAsString(response);
  // Then
  assertTrue(json.contains("v2"));
  assertTrue(json.contains("Oops"));
} // End

We can verify that the serialized response contains a non-null failure value and a null success value.

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

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 inspect 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 cannot create new result objects because Result is an interface, not a concrete type.

/** Test deserialization problem */
@Test
void testDeserializationProblem() {
  // Given
  String json = "{\"version\":\"v3\",\"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 Implementation

What we want, is for Jackson to treat Result values as JSON objects that contain either a success or a failure value. Fortunately, there’s a Jackson module that can solve this problem.

Registering the Jackson 3.x Datatype Module for Result

Once we have added Result-Jackson3 as a dependency, all we need to do is register ResultModule with our object mapper.

/*  Register ResultModule */
JsonMapper.Builder builder = JsonMapper.builder(); // Create new builder
builder.addModule(new ResultModule()); // Register manually
ObjectMapper objectMapper = builder.build(); // Create new object mapper

Alternatively, you can also make Jackson auto-discover the module.

/*  Register ResultModule */
builder.findAndAddModules(); // Register automatically

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

Deserializing Results

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 deserializeSuccessfulResult() {
  // Given
  String json = "{\"version\":\"v4\",\"result\":{\"success\":\"Yay\"}}";
  // When
  ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
  ApiResponse response = objectMapper.readValue(json, ApiResponse.class);
  // Then
  assertEquals("v4", 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 deserializeFailedResult() {
  // Given
  String json = "{\"version\":\"v5\",\"result\":{\"failure\":\"Nay\"}}";
  // When
  ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
  ApiResponse response = objectMapper.readValue(json, ApiResponse.class);
  // Then
  assertEquals("v5", response.getVersion());
  assertEquals("Nay", response.getResult().getFailure().orElse(null));
} // End

Conclusion

You have learned how to use results with Jackson without any problems by leveraging the Jackson 3.x datatype module for Result, demonstrating how it enables Jackson to treat Result objects as ordinary fields.

The full source code for the examples is available on GitHub.