Wednesday, August 1, 2018

Custom Jackson serializer for a generic tree

Leave a Comment

Say I have a parametrized tree implemented in Java as follows:

public class Tree<E> {    private static class Node {       E element;       List<Node> children.    }     Node root;     //... You get the idea. } 

The idea here is that the implementation above is only concerned with the topology of the tree, but does not know anything about the elements that will be stored in the tree by an instantiation.

Now, say I want my tree elements to be geographies. The reason they are organized in trees is because continents contain countries, countries contain states or a provinces, and so on. For simplicity, a geography has a name and a type:

public class GeoElement { String name; String type; } 

So that, finally, the geo hierarchy looks like so:

public class Geography extends Tree<GeoElement> {} 

Now to Jackson serialization. Assuming the Jackson serializer can see the fields, the direct serialization of this implementation will look like this:

{    "root": {       "element": {          "name":"Latin America",          "type":"Continent"       }       "children": [           {              "element": {                 "name":"Brazil",                 "type":"Country"              },              "children": [                  // ... A list of states in Brazil              ]           },           {              "element": {                 "name":"Argentina",                 "type":"Country"              },              "children": [                  // ... A list of states in Argentina              ]           }       ]    } 

This JSON rendering is no good because it contains the unnecessary artifacts from the Tree and Node classes, i.e. "root" and "element". What I need instead is this:

{    "name":"Latin America",    "type":"Continent"    "children": [        {           "name":"Brazil",           "type":"Country"           "children": [              // ... A list of states in Brazil           ]        },        {           "name":"Argentina",           "type":"Country"           "children": [              // ... A list of states in Argentina           ]        }    ] } 

Any help is most appreciated. -Igor.

4 Answers

Answers 1

What you need is @JsonUnwrapped.

Annotation used to indicate that a property should be serialized "unwrapped"; that is, if it would be serialized as JSON Object, its properties are instead included as properties of its containing Object

Add this annotation to the root field of Tree & element field of Node classes as follows:

public class Tree<E> {    private static class Node {        @JsonUnwrapped       E element;       List<Node> children.    }     @JsonUnwrapped    Node root;     //... You get the idea. } 

And it will give you your desired output:

{     "name": "Latin America",     "type": "Continent",     "children": [{         "name": "Brazil",         "type": "Country",         "children": []     }, {         "name": "Argentina",         "type": "Country",         "children": []     }] } 

Answers 2

Perhaps use @JsonValue like so:

public class Tree<E> {   @JsonValue   Node root; } 

if all you need is to just "unwrap" your tree?

Answers 3

Your best bet will be to build and register a custom serializer for your objects.

Define your serializer:

public class NodeSerializer extends StdSerializer<Node> { 

Then on your Node class:

@JsonSerialize(using = NodeSerializer.class) public class Node { } 

And inside of the NodeSerializer

@Override     public void serialize(       Node node, JsonGenerator jgen, SerializerProvider provider)        throws IOException, JsonProcessingException {          jgen.writeStartObject();         jgen.writeStringField("name", node.element.name);          jgen.writeStringField("type", node.element.type);         //Process children         serializeFields(node, jgen, provider);         jgen.writeEndObject();     } 

This general framework will let you control how the elements get serialized. You may need to @JsonIgnore the element object inside of the Node as well since your custom serializer is taking care of pushing that info into the resulting JSON. There is a lot online about custom serializers and overriding default JSON export.

You can get rid of root in a similar way with a serializer for the Tree implementation.

If you don't want to register the serializer on the class you can also do it on a one at a time basis using the ObjectMapper:

ObjectMapper mapper = new ObjectMapper();  SimpleModule module = new SimpleModule(); module.addSerializer(Node.class, new NodeSerializer()); mapper.registerModule(module);  String serialized = mapper.writeValueAsString(tree); 

The annotation approach will apply globally. This approach allows some control of how/where your custom serializer is used.

Answers 4

For removing the element type, one possibility would be to change your structure so that the name and the type will be directly included in each node:

public class TreeGeo {    private static class Node {       String name;       String type;       List<Node> children.    }     Node root; } 

For removing the root type, I don't know. I suppose that you could extract a sub-object from the jsonObject but I don't know much about Jackson. However, you could give it a better name like world or manipulate the resulting string to remove it manually with some string manipulations.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment