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