Last active
May 29, 2019 19:46
-
-
Save mandhor/0443ddf151ab2bdbdffc to your computer and use it in GitHub Desktop.
[ SOLUTION ] JSON REST API returning List of different objects - source of LinkedTreeMap problem when deserializing data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Source of problem | |
// JSON output from API | |
[ | |
{ | |
"id": 1, | |
"created_at": "01-06-2014", | |
"type": "message", | |
"text": "Hi there!", | |
"from": { | |
"id": 2, | |
"created_at": "10-03-2015", | |
"type": "user", | |
"name": "john smith", | |
"photo_url": "http://example.com/john.jpg", | |
"detail": { | |
"some_id": 123, | |
"some_data": "data", | |
"some_other_data": "more data" | |
} | |
} | |
}, | |
{ | |
"id": 2, | |
"created_at": "10-03-2015", | |
"type": "user", | |
"name": "john smith", | |
"photo_url": "http://example.com/john.jpg", | |
"detail": { | |
"some_id": 123, | |
"some_data": "data", | |
"some_other_data": "more data" | |
} | |
}, | |
{ | |
"id": 3, | |
"created_at": "19-03-2015", | |
"type": "notification", | |
"priority": 1, | |
"detail": { | |
"id": 234, | |
"valid_until": "15-05-2015", | |
"some_notification_data": "notification data" | |
} | |
} | |
] | |
// ----- | |
// Usage | |
RuntimeTypeAdapterFactory<Item> itemAdapter = | |
RuntimeTypeAdapterFactory.of(Item.class, new ItemTypePredicate()) | |
.registerSubtype(User.class) | |
.registerSubtype(Message.class) | |
.registerSubtype(Notification.class); | |
final Gson gson = new GsonBuilder() | |
.enableComplexMapKeySerialization() | |
.registerTypeAdapterFactory(itemAdapter).create(); | |
//this might be useful when defining data format returned from api in your rest client | |
Type itemsJsonListType = new TypeToken<List<JsonObject>>() {}.getType(); | |
//retrieve List<JsonObject> from api response | |
ArrayList<JsonObject> response = getApiResponse(); | |
ArrayList<Item> mItems = new ArrayList<>(); | |
for(JsonObject o : response) { | |
Item i = gson.fromJson(o, Item.class); | |
mItems.add(i); | |
} | |
// ----- | |
// Classes needed | |
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { | |
private final Class<?> baseType; | |
private final RuntimeTypeAdapterPredicate predicate; | |
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>(); | |
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>(); | |
private RuntimeTypeAdapterFactory(Class<?> baseType, RuntimeTypeAdapterPredicate predicate) { | |
if (predicate == null || baseType == null) { | |
throw new NullPointerException(); | |
} | |
this.baseType = baseType; | |
this.predicate = predicate; | |
} | |
/** | |
* Creates a new runtime type adapter using for {@code baseType} using {@code | |
* typeFieldName} as the type field name. Type field names are case sensitive. | |
*/ | |
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, RuntimeTypeAdapterPredicate predicate) { | |
return new RuntimeTypeAdapterFactory<T>(baseType, predicate); | |
} | |
/** | |
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as | |
* the type field name. | |
*/ | |
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { | |
return new RuntimeTypeAdapterFactory<T>(baseType, null); | |
} | |
/** | |
* Registers {@code type} identified by {@code label}. Labels are case | |
* sensitive. | |
* | |
* @throws IllegalArgumentException if either {@code type} or {@code label} | |
* have already been registered on this type adapter. | |
*/ | |
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { | |
if (type == null || label == null) { | |
throw new NullPointerException(); | |
} | |
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { | |
throw new IllegalArgumentException("types and labels must be unique"); | |
} | |
labelToSubtype.put(label, type); | |
subtypeToLabel.put(type, label); | |
return this; | |
} | |
/** | |
* Registers {@code type} identified by its {@link Class#getSimpleName simple | |
* name}. Labels are case sensitive. | |
* | |
* @throws IllegalArgumentException if either {@code type} or its simple name | |
* have already been registered on this type adapter. | |
*/ | |
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { | |
return registerSubtype(type, type.getSimpleName()); | |
} | |
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { | |
if (type.getRawType() != baseType) { | |
return null; | |
} | |
final Map<String, TypeAdapter<?>> labelToDelegate | |
= new LinkedHashMap<String, TypeAdapter<?>>(); | |
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate | |
= new LinkedHashMap<Class<?>, TypeAdapter<?>>(); | |
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { | |
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); | |
labelToDelegate.put(entry.getKey(), delegate); | |
subtypeToDelegate.put(entry.getValue(), delegate); | |
} | |
return new TypeAdapter<R>() { | |
@Override public R read(JsonReader in) throws IOException { | |
JsonElement jsonElement = Streams.parse(in); | |
String label = predicate.process(jsonElement); | |
@SuppressWarnings("unchecked") // registration requires that subtype extends T | |
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); | |
if (delegate == null) { | |
throw new JsonParseException("cannot deserialize " + baseType + " subtype named " | |
+ label + "; did you forget to register a subtype?"); | |
} | |
return delegate.fromJsonTree(jsonElement); | |
} | |
@Override public void write(JsonWriter out, R value) throws IOException { // Unimplemented as we don't use write. | |
/*Class<?> srcType = value.getClass(); | |
String label = subtypeToLabel.get(srcType); | |
@SuppressWarnings("unchecked") // registration requires that subtype extends T | |
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); | |
if (delegate == null) { | |
throw new JsonParseException("cannot serialize " + srcType.getName() | |
+ "; did you forget to register a subtype?"); | |
} | |
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); | |
if (jsonObject.has(typeFieldName)) { | |
throw new JsonParseException("cannot serialize " + srcType.getName() | |
+ " because it already defines a field named " + typeFieldName); | |
} | |
JsonObject clone = new JsonObject(); | |
clone.add(typeFieldName, new JsonPrimitive(label)); | |
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { | |
clone.add(e.getKey(), e.getValue()); | |
}*/ | |
Streams.write(null, out); | |
} | |
}; | |
} | |
} | |
// ----- | |
public abstract class RuntimeTypeAdapterPredicate { | |
public abstract String process(JsonElement element); | |
} | |
// ----- | |
public class ItemTypePredicate extends RuntimeTypeAdapterPredicate { | |
@Override | |
public String process(JsonElement element) { | |
JsonObject obj = element.getAsJsonObject(); | |
String itemType = obj.get("type").getAsString(); | |
if(itemType.contentEquals("user")) | |
return "User"; | |
if(itemType.contentEquals("message")) | |
return "Message"; | |
if(itemType.contentEquals("notification")) | |
return "Notification"; | |
return "Item"; | |
} | |
} | |
// ----- | |
// Our models | |
public class Item { | |
public long id; | |
public long created_at; | |
public String type; | |
} | |
public class User extends Item { | |
public String name; | |
public String photo_url; | |
public ExtendedUserInfo detail; | |
} | |
public class Message extends Item { | |
public String text; | |
public User from; | |
} | |
public class Notification extends Item { | |
public int priority; | |
public NotificationDetail detail; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment