4

I'm trying to deserialize a response using Gson. The data consists of lists of nodes which may be nested to arbitrary depths. The json looks something like this:

{
    "type": "node",
    "children": [
        {
            "id": "abc123",
            "name": "Name 1",
            "subdata": {
                "type": "node",
                "children": [
                    {
                        "id": "def456",
                        "name": "Name 2"
                    }
                ]
            }
        }
    ]
}

Now, without any custom type adapters, I can make this work with the following classes:

public class Data {
    private String type;
    private List<Node> nodes;
}

public class Node {
    private String id;
    private String name;
    private Data subdata;
}

Everything works fine and dandy for now. However, the server might cut some of the deeper nodes and respond only with their IDs, so the subdata might look like this instead:

"subdata": {
    "type": "extra",
    "children": ["ghi", "jkl", "mno"]
}

This of course could be represented as a Java class like this:

public class ExtraData {
    private String type;
    private List<String> children;
}

The question is, though: How do I handle the deserialization so that the subdata could be either Data or ExtraData?

manabreak
  • 5,415
  • 7
  • 39
  • 96

1 Answers1

1

The children of the given nodes seem to be JSON arrays always, so the first thing you could do with them is declaring the children as List<?> hiding the actual type. However, you still have the type property/field that is perfectly fine to get the actual type of the children. The simplest way is probably just adding another JSON deserializer in order to deserialize Data instances with some performance costs (since these are not type adapters) and, as far as I know, lack of @SerializedName on the fields of the Data class.

If you're also fine with changing your DTOs types, prefer enums rather than raw strings as they work just perfect with enums (especially in cooperation with smart IDEs):

enum Type {

    @SerializedName("node")
    NODE,

    @SerializedName("extra")
    EXTRA

}

The Data class itself then might look like as follows:

final class Data {

    private final Type type;
    private final List<?> children; // this one is supposed to be:
                                    // * either List<String> if type=EXTRA
                                    // * or List<Node> if type=NODE

    Data(final Type type, final List<?> children) {
        this.type = type;
        this.children = children;
    }

    Type getType() {
        return type;
    }

    List<?> getChildren() {
        return children;
    }

}

Since the extra-typed children are just strings in your question, just add the node DTO class:

final class Node {

    @SerializedName("id")
    private final String id = null;

    @SerializedName("name")
    private final String name = null;

    @SerializedName("subdata")
    private final Data subdata = null;

    String getId() {
        return id;
    }

    String getName() {
        return name;
    }

    Data getSubdata() {
        return subdata;
    }

}

Now, while deserializing the Data class, you can determine the actual type of the children list and, according to the node type, deserialize it as either a list of strings or a list of nodes. Note that the deserializer below uses java.lang.reflect.Type instances rather than java.lang.Class because the latter is weak due to Java generic type erasure and is List.class for any list parameterization (strings, nodes, etc). Having the expected types provided with type tokens, just delegate a JSON key/value pair to the deserialization context specifying the target type thus making recursive deserialization that would work for arbitrary nested elements level (however, GSON has some internal stack limit that's limited to 32 if I'm not mistaken).

final class DataJsonDeserializer
        implements JsonDeserializer<Data> {

    private static final JsonDeserializer<Data> dataJsonDeserializer = new DataJsonDeserializer();

    private static final java.lang.reflect.Type nodeListType = new TypeToken<List<Node>>() {
    }.getType();

    private static final java.lang.reflect.Type stringListType = new TypeToken<List<String>>() {
    }.getType();

    private DataJsonDeserializer() {
    }

    static JsonDeserializer<Data> getDataJsonDeserializer() {
        return dataJsonDeserializer;
    }

    @Override
    public Data deserialize(final JsonElement jsonElement, final java.lang.reflect.Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final JsonObject rootJsonObject = jsonElement.getAsJsonObject();
        final Type nodeType = context.deserialize(rootJsonObject.get("type"), Type.class);
        final JsonArray childrenJsonArray = rootJsonObject.get("children").getAsJsonArray();
        final List<?> children;
        switch ( nodeType ) {
        case NODE:
            children = context.deserialize(childrenJsonArray, nodeListType);
            break;
        case EXTRA:
            children = context.deserialize(childrenJsonArray, stringListType);
            break;
        default:
            throw new AssertionError(nodeType);
        }
        return new Data(nodeType, children);
    }

}

And the demo that recursively walks through the children (note the enhanced for statements that cast each item to the target type below):

public final class EntryPoint {

    private static final String JSON_WITH_SUBNODES = "{\"type\":\"node\",\"children\":[{\"id\":\"abc123\",\"name\":\"Name 1\",\"subdata\":{\"type\":\"node\",\"children\":[{\"id\":\"def456\",\"name\":\"Name 2\"}]}}]}";
    private static final String JSON_WITH_REFERENCES = "{\"type\":\"node\",\"children\":[{\"id\":\"abc123\",\"name\":\"Name 1\",\"subdata\":{\"type\":\"extra\",\"children\":[\"ghi\",\"jkl\",\"mno\"]}}]}";

    private static final Gson gson = new GsonBuilder()
            .registerTypeAdapter(Data.class, getDataJsonDeserializer())
            .create();

    public static void main(final String... args) {
        process(gson.fromJson(JSON_WITH_SUBNODES, Data.class));
        process(gson.fromJson(JSON_WITH_REFERENCES, Data.class));
    }

    private static void process(final Data data) {
        process(data, 0);
        out.println();
    }

    private static void process(final Data data, final int level) {
        for ( int i = 0; i < level; i++ ) {
            out.print('>');
        }
        final List<?> children = data.getChildren();
        final Type type = data.getType();
        out.println(type);
        switch ( type ) {
        case NODE:
            @SuppressWarnings("unchecked")
            final Iterable<Node> nodeChildren = (Iterable<Node>) children;
            for ( final Node node : nodeChildren ) {
                out.printf("\t%s %s\n", node.getId(), node.getName());
                final Data subdata = node.getSubdata();
                if ( subdata != null ) {
                    process(subdata, level + 1);
                }
            }
            break;
        case EXTRA:
            @SuppressWarnings("unchecked")
            final Iterable<String> extraChildren = (Iterable<String>) children;
            for ( final String extra : extraChildren ) {
                out.printf("\t%s\n", extra);
            }
            break;
        default:
            throw new AssertionError(type);
        }
    }

}

The output:

NODE
    abc123 Name 1
>NODE
    def456 Name 2

NODE
    abc123 Name 1
>EXTRA
    ghi
    jkl
    mno
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105