1
0
Files
jackson3-by-example/docs/part4-custom.md
Ankur c438afc33b Jackson 3 series companion code
37 runnable examples covering the eight feature posts on ankurm.com, verified
against Jackson 3.2.1 on Temurin 21.0.5. Every output committed under docs/ was
produced by run-all.sh.

Also documents 11 places where the published snippets do not compile or do not
behave as printed against a real Jackson 3 build - most notably that
writeValueAsString(List<Base>) silently drops the polymorphic type discriminator,
so the post's serialised output cannot be read back.
2026-08-04 23:29:26 +05:30

3.2 KiB

Part 4 — Custom serialisers, deserialisers and mix-ins

Post: https://ankurm.com/jackson-custom-serializer-mixin/

This is the part with the most API churn. The post's code is Jackson 2 throughout.

Jackson 2 Jackson 3
JsonSerializer<T> ValueSerializer<T>
JsonDeserializer<T> ValueDeserializer<T>
SerializerProvider SerializationContext
com.fasterxml.jackson.databind.ser.std.StdSerializer tools.jackson.databind.ser.std.StdSerializer
gen.writeNumberField / writeStringField gen.writeNumberProperty / writeStringProperty
p.getCodec().readTree(p) ctxt.readTree(p)
throws IOException removed — exceptions are unchecked
new SimpleModule(name, Version) new SimpleModule(name)
mapper.registerModule(m) builder.addModule(m)
mapper.addMixIn(a, b) builder.addMixIn(a, b)

E01 / E02 — the handlers

E01MoneyValueSerializer.java · E02MoneyValueDeserializer.java · Money.java

The deserialiser uses path() rather than get(). The post's version calls rootNode.get("amount").decimalValue(), which throws NullPointerException on {"currency":"USD"}. There is a second trap: in Jackson 3, a bare decimalValue() on a MissingNode throws, where Jackson 2 returned BigDecimal.ZERO. The defaulting overload decimalValue(BigDecimal.ZERO) is what you want.

E03 — registration

E03SimpleModuleRegistration.java

serialised   : {"amount":20.00,"currency":"USD"}
amount       : 20.0  (scale lost)
amount exact : 20.00  (scale preserved)
missing field: Money[amount=0, currencyCode=EUR]
no module    : {"amount":19.999,"currencyCode":"usd"}

The post prints 20.00 for the deserialised amount. You get 20.0 — Jackson parses the literal as a double before handing it over, so the scale is gone. If scale matters, and for money it does, enable USE_BIG_DECIMAL_FOR_FLOATS, as the third line shows.

E04 — mix-ins

E04MixinAnnotations.java

with mixin   : {"city":"Springfield","street":"123 Main St","zip":"12345"}
without mixin: {"city":"Springfield","internalTrackingCode":"INTERNAL-X99","postalCode":"12345","street":"123 Main St"}

The second line is the proof that nothing was modified at the bytecode level: a mapper built without the mix-in still sees internalTrackingCode and postalCode.

E05 — ValueSerializer directly

E05ValueSerializerDirect.java

Beyond the post. StdSerializer kept its name, which hides the rename; extending ValueSerializer directly makes it obvious, and is the leaner form for a simple wrapper.

custom  : {"assignee":"u-42","title":"Fix build"}
default : {"assignee":{"value":"u-42"},"title":"Fix build"}
base class: tools.jackson.databind.ValueSerializer