|
| 1 | +--- |
| 2 | +date: 2024-05-30 |
| 3 | +title: Using Jackson Deduction to Simplify Deserialisation |
| 4 | +cover: |
| 5 | + image: posts/2024-05-30-using-jackson-deduction-to-simplify-deserialisation/cover.jpeg |
| 6 | +tags: |
| 7 | +- java |
| 8 | +- jackson |
| 9 | +--- |
| 10 | + |
| 11 | +I find the documentation for Jackson is on the terse side, and dare I say |
| 12 | +obstructively self-referential. If you aren't moved fully to tears by the Javadoc |
| 13 | +entries for `JsonTypeInfo.As`, you deserve an ACM award. |
| 14 | + |
| 15 | +In short: I need a helpful and up-to-date guide, so I'm writing one. |
| 16 | + |
| 17 | +== What are we trying to do? |
| 18 | + |
| 19 | +We often want to vary the content of our JSON. It's integral to the https://www.enterpriseintegrationpatterns.com/patterns/messaging/EnvelopeWrapper.html[Envelope pattern] for instance. Varying the content looks like this. |
| 20 | + |
| 21 | +Here's an example of an intergalactic animal. |
| 22 | + |
| 23 | +[source,json] |
| 24 | +---- |
| 25 | +{ |
| 26 | + "created": "1977-05-25T12:00:00Z", |
| 27 | + "animal": { |
| 28 | + "galaxy": "Far Far Away", |
| 29 | + "name": "Womp Rat" |
| 30 | + } |
| 31 | +} |
| 32 | +---- |
| 33 | + |
| 34 | +And here's an example of a provincial animal. |
| 35 | + |
| 36 | +[source,json] |
| 37 | +---- |
| 38 | +{ |
| 39 | + "created": "1835-09-15T06:00:00Z", |
| 40 | + "animal": { |
| 41 | + "province": "Galápagos Islands", |
| 42 | + "name": "Galápagos tortoise" |
| 43 | + } |
| 44 | +} |
| 45 | +---- |
| 46 | + |
| 47 | +In our Java code, we want to be able to deserialise Animals, and we want to handle either `IntergalacticAnimals`, |
| 48 | +or `ProvincialAnimals`. We might write some transport code that looks like this. |
| 49 | + |
| 50 | +[source,java] |
| 51 | +---- |
| 52 | +record AnimalSpottedEvent<A extends Animal>(Instant created, A animal) {} |
| 53 | +
|
| 54 | +interface Animal {} |
| 55 | +
|
| 56 | +record IntergalacticAnimal(String galaxy, String name) implements Animal {} |
| 57 | +
|
| 58 | +record ProvincialAnimal(String province, String name) implements Animal {} |
| 59 | +---- |
| 60 | + |
| 61 | +When it's called to handle deserialisation of an Animal over the wire, Jackson needs to work out |
| 62 | +what's the "message inside the envelope?" Is it an `IntergalacticAnimal`, or is it a `ProvincialAnimal`? |
| 63 | +Without this knowledge, Jackson can't instantiate the right kind of object. |
| 64 | + |
| 65 | +There is a (truly excellent) resource, https://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html[Deserialize JSON with Jackson into Polymorphic Types - A Complete Example], |
| 66 | +which describes how to do this using `JsonTypeInfo`, but it was written in 2011 for Jackson 1.7. |
| 67 | + |
| 68 | +== State of the Art |
| 69 | + |
| 70 | +You may have missed the 2.12.0 release of Jackson Databind in Novemeber 2020. The world was |
| 71 | +generally busy with other things around then. In which case the https://fasterxml.github.io/jackson-annotations/javadoc/2.12/com/fasterxml/jackson/annotation/JsonTypeInfo.Id.html#DEDUCTION[`JsonTypeInfo.Id#DEDUCTION`] |
| 72 | +could have slipped by you unnoticed. |
| 73 | + |
| 74 | +The newer "deduction" mode takes away the need to tell Jackson _how_ to read the incoming data; instead |
| 75 | +Jackson will take a list of possible choices you give it, and it will determine the best match for |
| 76 | +deserialisation. |
| 77 | + |
| 78 | +What does that look like? You can follow these steps: |
| 79 | + |
| 80 | +1. add `@JsonTypeInfo(use = DEDUCTION)` to the envelope property |
| 81 | +2. add `@JsonSubTypes(@Type(...))` |
| 82 | + |
| 83 | +Here is our updated example code. |
| 84 | + |
| 85 | +[source,java] |
| 86 | +---- |
| 87 | +record AnimalSpottedEvent<A extends Animal>( |
| 88 | + Instant created, |
| 89 | + @JsonTypeInfo(use = DEDUCTION) |
| 90 | + @JsonSubTypes({ |
| 91 | + @Type(IntergalacticAnimal.class), |
| 92 | + @Type(ProvincialAnimal.class)}) |
| 93 | + A animal) {} |
| 94 | +
|
| 95 | +interface Animal {} |
| 96 | +
|
| 97 | +record IntergalacticAnimal(String galaxy, String name) implements Animal {} |
| 98 | +
|
| 99 | +record ProvincialAnimal(String province, String name) implements Animal {} |
| 100 | +---- |
| 101 | + |
| 102 | +https://github.com/TomRegan/jackson-deduction-demo[I've created a demo project] as a companion to provide a working example. |
| 103 | + |
| 104 | +Happy deserialisation! |
| 105 | + |
| 106 | +== Appendix |
| 107 | + |
| 108 | +=== Dependencies |
| 109 | + |
| 110 | +`jackson-annotations` provides `@JsonTypeInfo` and associated annotations which you'll use in your transport code. |
| 111 | + |
| 112 | +`jackson-databind` provides the "invisible" deserialisers. You'll need these available on the classpath in your server |
| 113 | +or client application. |
| 114 | + |
| 115 | +There are any number of ways to get `jackson-annotations` and `jackson-databind` on your classpath. `spring-boot-starter-web` |
| 116 | +includes many Jackson components; alternatively the Jackson project provides a https://mvnrepository.com/artifact/com.fasterxml.jackson/jackson-bom[BOM]. |
0 commit comments