Jackson Integration

Runnable example: JacksonSample.java

The fun-jackson module provides Jackson serializers and deserializers for all dmx-fun types. It is an optional dependency — the core fun library has no runtime dependency on Jackson.

Adding the dependency

fun-jackson declares jackson-databind as compileOnly. You must add jackson-databind explicitly to your own classpath. Any version from 2.13.x through 2.21.x is supported (see version compatibility below).

// Gradle
implementation("codes.domix:fun-jackson:0.0.13")
// Jackson itself — bring your own version (2.13.x – 2.21.x)
implementation("com.fasterxml.jackson.core:jackson-databind:2.21.2")
<!-- Maven -->
<dependency>
<groupId>codes.domix</groupId>
<artifactId>fun-jackson</artifactId>
<version>0.0.13</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.21.2</version>
</dependency>

Registering DmxFunModule

// Manual registration
ObjectMapper mapper = new ObjectMapper()
.registerModule(new DmxFunModule());
// Auto-discovery (recommended) — works because DmxFunModule is registered
// as a com.fasterxml.jackson.databind.Module service provider via META-INF
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// Spring Boot — register as a bean; Spring auto-applies it to the mapper
@Bean
public DmxFunModule dmxFunModule() {
return new DmxFunModule();
}

DmxFunModule is registered as a com.fasterxml.jackson.databind.Module service provider, so findAndRegisterModules() picks it up automatically without any explicit wiring.

JSON shapes

TypeSuccess / presentFailure / absent
Option<T>v (unwrapped)null
Result<V, E>{"ok": v}{"err": e}
Try<V>v (unwrapped){"error": "message"}
Validated<E, A>{"valid": a}{"invalid": e}
Either<L, R>{"right": v}{"left": v}
Tuple2<A, B>[a, b]
Tuple3<A, B, C>[a, b, c]
Tuple4<A, B, C, D>[a, b, c, d]
NonEmptyList<T>[head, ...tail]

Round-trip examples

Option<T>

ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// Serialize
String some = mapper.writeValueAsString(Option.some("hello")); // "hello"
String none = mapper.writeValueAsString(Option.none()); // null
// Deserialize — TypeReference required to carry the type parameter
Option<String> opt = mapper.readValue("\"hello\"",
new TypeReference<Option<String>>() {});
// Some("hello")
Option<String> absent = mapper.readValue("null",
new TypeReference<Option<String>>() {});
// None

Result<V, E>

ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// Serialize
String ok = mapper.writeValueAsString(Result.ok(42)); // {"ok":42}
String err = mapper.writeValueAsString(Result.err("oops")); // {"err":"oops"}
// Deserialize
Result<Integer, String> okResult = mapper.readValue("{\"ok\":42}",
new TypeReference<Result<Integer, String>>() {});
// Ok(42)
Result<Integer, String> errResult = mapper.readValue("{\"err\":\"oops\"}",
new TypeReference<Result<Integer, String>>() {});
// Err("oops")

Try<V>

ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// Serialize
String success = mapper.writeValueAsString(Try.success("data")); // "data"
String failure = mapper.writeValueAsString(
Try.failure(new IllegalArgumentException("bad input"))); // {"error":"bad input"}
// Deserialize — success
Try<String> ok = mapper.readValue("\"data\"",
new TypeReference<Try<String>>() {});
// Success("data")
// Deserialize — failure; always reconstructed as RuntimeException
Try<String> failed = mapper.readValue("{\"error\":\"bad input\"}",
new TypeReference<Try<String>>() {});
// Failure(RuntimeException("bad input"))

Validated, Either, Tuple*, NonEmptyList

ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// Validated
String valid = mapper.writeValueAsString(Validated.valid(42)); // {"valid":42}
String invalid = mapper.writeValueAsString(Validated.invalid("bad")); // {"invalid":"bad"}
Validated<String, Integer> v = mapper.readValue("{\"valid\":42}",
new TypeReference<Validated<String, Integer>>() {});
// Valid(42)
// Either
String right = mapper.writeValueAsString(Either.right("ok")); // {"right":"ok"}
String left = mapper.writeValueAsString(Either.left("err")); // {"left":"err"}
Either<String, String> e = mapper.readValue("{\"right\":\"ok\"}",
new TypeReference<Either<String, String>>() {});
// Right("ok")
// Tuple2 / Tuple3 / Tuple4 — serialized as JSON arrays
String t2 = mapper.writeValueAsString(new Tuple2<>("Alice", 30)); // ["Alice",30]
String t3 = mapper.writeValueAsString(Tuple3.of("Alice", 30, true)); // ["Alice",30,true]
String t4 = mapper.writeValueAsString(Tuple4.of("Alice", 30, true, 1.75)); // ["Alice",30,true,1.75]
// NonEmptyList — serialized as a JSON array
String nel = mapper.writeValueAsString(
NonEmptyList.of("a", List.of("b", "c"))); // ["a","b","c"]
NonEmptyList<String> nelBack = mapper.readValue("[\"a\",\"b\",\"c\"]",
new TypeReference<NonEmptyList<String>>() {});
// NonEmptyList["a","b","c"]

Spring Boot and JAX-RS

// Spring Boot — declare DmxFunModule as a @Bean; Spring applies it automatically
// to the auto-configured ObjectMapper.
@Configuration
public class JacksonConfig {
@Bean
public DmxFunModule dmxFunModule() {
return new DmxFunModule();
}
}
// Controller returning Result<User, ApiError> — Jackson serializes it transparently
@GetMapping("/users/{id}")
public Result<User, ApiError> getUser(@PathVariable Long id) {
return userService.findById(id); // {"ok":{...}} or {"err":{...}}
}
// JAX-RS — register DmxFunModule on the ObjectMapper provider
@Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper =
new ObjectMapper().registerModule(new DmxFunModule());
@Override
public ObjectMapper getContext(Class<?> type) { return mapper; }
}

Version compatibility

The module is tested in CI against the following Jackson versions on every pull request that touches the jackson/ module:

Jackson versionStatus
2.13.xtested
2.14.xtested
2.15.xtested
2.16.xtested
2.17.xtested
2.18.xtested
2.19.xtested
2.20.xtested
2.21.xtested

To test locally against a specific version:

Terminal window
./gradlew :jackson:test -PjacksonVersion=2.15.4

Known limitations

Try.failure loses the exception type

Try.failure(ex) serializes as {"error": "message"} — only the exception message is preserved. On deserialization, a RuntimeException is always reconstructed regardless of the original exception class. Stack traces are not serialized.

Workaround: convert Try<V> to Result<V, String> via toResult(Throwable::getMessage) before serializing if the exception type matters to consumers.

Try.success with object values that have a single "error" string field

The deserializer detects a Failure by checking whether the JSON object has exactly one field named "error" containing a string. If a success value is a POJO that happens to serialize as {"error": "some text"} (one string field named "error"), it will be misread as a Failure on deserialization.

Workaround: avoid using Try<V> for types whose JSON shape is {"error": "<string>"}. Use Result<V, E> instead, which uses an explicit "ok" / "err" envelope that cannot be confused with value content.

Type parameters require TypeReference

Without a TypeReference, Jackson loses the generic type information at deserialization time and returns raw LinkedHashMap / ArrayList instead of the intended element types. Always use TypeReference<Option<String>>(), TypeReference<Result<Integer, String>>(), etc.