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).
// Gradleimplementation("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 registrationObjectMapper 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-INFObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// Spring Boot — register as a bean; Spring auto-applies it to the mapper@Beanpublic 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
| Type | Success / present | Failure / 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();
// SerializeString some = mapper.writeValueAsString(Option.some("hello")); // "hello"String none = mapper.writeValueAsString(Option.none()); // null
// Deserialize — TypeReference required to carry the type parameterOption<String> opt = mapper.readValue("\"hello\"", new TypeReference<Option<String>>() {});// Some("hello")
Option<String> absent = mapper.readValue("null", new TypeReference<Option<String>>() {});// NoneResult<V, E>
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// SerializeString ok = mapper.writeValueAsString(Result.ok(42)); // {"ok":42}String err = mapper.writeValueAsString(Result.err("oops")); // {"err":"oops"}
// DeserializeResult<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();
// SerializeString success = mapper.writeValueAsString(Try.success("data")); // "data"String failure = mapper.writeValueAsString( Try.failure(new IllegalArgumentException("bad input"))); // {"error":"bad input"}
// Deserialize — successTry<String> ok = mapper.readValue("\"data\"", new TypeReference<Try<String>>() {});// Success("data")
// Deserialize — failure; always reconstructed as RuntimeExceptionTry<String> failed = mapper.readValue("{\"error\":\"bad input\"}", new TypeReference<Try<String>>() {});// Failure(RuntimeException("bad input"))Validated, Either, Tuple*, NonEmptyList
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
// ValidatedString 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)
// EitherString 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 arraysString 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 arrayString 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.@Configurationpublic 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@Providerpublic 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 version | Status |
|---|---|
| 2.13.x | tested |
| 2.14.x | tested |
| 2.15.x | tested |
| 2.16.x | tested |
| 2.17.x | tested |
| 2.18.x | tested |
| 2.19.x | tested |
| 2.20.x | tested |
| 2.21.x | tested |
To test locally against a specific version:
./gradlew :jackson:test -PjacksonVersion=2.15.4Known 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.