Converting changing JSON api with GSON [duplicate] - java

I've got the following classes
public class MyClass {
private List<MyOtherClass> others;
}
public class MyOtherClass {
private String name;
}
And I have JSON that may look like this
{
others: {
name: "val"
}
}
or this
{
others: [
{
name: "val"
},
{
name: "val"
}
]
}
I'd like to be able to use the same MyClass for both of these JSON formats. Is there a way to do this with Gson?

I came up with an answer.
private static class MyOtherClassTypeAdapter implements JsonDeserializer<List<MyOtherClass>> {
public List<MyOtherClass> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx) {
List<MyOtherClass> vals = new ArrayList<MyOtherClass>();
if (json.isJsonArray()) {
for (JsonElement e : json.getAsJsonArray()) {
vals.add((MyOtherClass) ctx.deserialize(e, MyOtherClass.class));
}
} else if (json.isJsonObject()) {
vals.add((MyOtherClass) ctx.deserialize(json, MyOtherClass.class));
} else {
throw new RuntimeException("Unexpected JSON type: " + json.getClass());
}
return vals;
}
}
Instantiate a Gson object like this
Type myOtherClassListType = new TypeToken<List<MyOtherClass>>() {}.getType();
Gson gson = new GsonBuilder()
.registerTypeAdapter(myOtherClassListType, new MyOtherClassTypeAdapter())
.create();
That TypeToken is a com.google.gson.reflect.TypeToken.
You can read about the solution here:
https://sites.google.com/site/gson/gson-user-guide#TOC-Serializing-and-Deserializing-Gener

Thank you three-cups for the solution!
The same thing with generic type in case it's needed for multiple types:
public class SingleElementToListDeserializer<T> implements JsonDeserializer<List<T>> {
private final Class<T> clazz;
public SingleElementToListDeserializer(Class<T> clazz) {
this.clazz = clazz;
}
public List<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
List<T> resultList = new ArrayList<>();
if (json.isJsonArray()) {
for (JsonElement e : json.getAsJsonArray()) {
resultList.add(context.<T>deserialize(e, clazz));
}
} else if (json.isJsonObject()) {
resultList.add(context.<T>deserialize(json, clazz));
} else {
throw new RuntimeException("Unexpected JSON type: " + json.getClass());
}
return resultList;
}
}
And configuring Gson:
Type myOtherClassListType = new TypeToken<List<MyOtherClass>>() {}.getType();
SingleElementToListDeserializer<MyOtherClass> adapter = new SingleElementToListDeserializer<>(MyOtherClass.class);
Gson gson = new GsonBuilder()
.registerTypeAdapter(myOtherClassListType, adapter)
.create();

to share code, and to only apply the deserialization logic to specific fields:
JSON model:
public class AdminLoginResponse implements LoginResponse
{
public Login login;
public Customer customer;
#JsonAdapter(MultiOrganizationArrayOrObject.class) // <-------- look here
public RealmList<MultiOrganization> allAccounts;
}
field-specific class:
class MultiOrganizationArrayOrObject
: ArrayOrSingleObjectTypeAdapter<RealmList<MultiOrganization>,MultiOrganization>(kClass()) {
override fun List<MultiOrganization>.toTypedList() = RealmList(*this.toTypedArray())
}
abstract class:
/**
* parsed field can either be a [JSONArray] of type [Element], or an single [Element] [JSONObject].
*/
abstract class ArrayOrSingleObjectTypeAdapter<TypedList: List<Element>, Element : Any>(
private val elementKClass: KClass<Element>
) : JsonDeserializer<TypedList> {
override fun deserialize(
json: JsonElement, typeOfT: Type?, ctx: JsonDeserializationContext
): TypedList = when {
json.isJsonArray -> json.asJsonArray.map { ctx.deserialize<Element>(it, elementKClass.java) }
json.isJsonObject -> listOf(ctx.deserialize<Element>(json, elementKClass.java))
else -> throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}.toTypedList()
abstract fun List<Element>.toTypedList(): TypedList
}

Building off three-cups answer, I have the following which lets the JsonArray be deserialized directly as an array.
static public <T> T[] fromJsonAsArray(Gson gson, JsonElement json, Class<T> tClass, Class<T[]> tArrClass)
throws JsonParseException {
T[] arr;
if(json.isJsonObject()){
//noinspection unchecked
arr = (T[]) Array.newInstance(tClass, 1);
arr[0] = gson.fromJson(json, tClass);
}else if(json.isJsonArray()){
arr = gson.fromJson(json, tArrClass);
}else{
throw new RuntimeException("Unexpected JSON type: " + json.getClass());
}
return arr;
}
Usage:
String response = ".......";
JsonParser p = new JsonParser();
JsonElement json = p.parse(response);
Gson gson = new Gson();
MyQuote[] quotes = GsonUtils.fromJsonAsArray(gson, json, MyQuote.class, MyQuote[].class);

Related

GSON: JSON deserialization to variable type (List/String)

At this point it's already an old question and I've probably read every related topic on SO.
But to the point. I need some advice or correction maybe?
For some reason we have generatable Jsons of 2 types:
{"data": {"id": "value"}} and {"data":[{"id": "value"}]}
Object and Array. There are also other params but they doesn't matter here. "id" is differ for every request. Sometimes it's userId, portfolioId etc. So I get "id" and pass it to related var.
For a long time I was working with the first case. And created POJO like this:
Data.class
public class Data {
#SerializedName("id")
#Expose
private String id;
public Data() {
}
public Data(String id) {
super();
this.id = id;
}
protected String getId() {
return id;
}
And I adress "data" paramets via User.class.
#JsonAdapter(UserDeserializer.class)
public Data data;
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
public User() {
}
public User(Data data) {
super();
this.data = data;
}
Gson gson = new Gson();
public String getPortfolioList(String tokenId, String userId) {
Call<User> call = apiRequest.getPortfolioList(userId, tokenId);
try {
User newResult = gson.fromJson(String.valueOf(call.execute().body()), User.class);
System.out.println(newResult.getData().getId());
} catch (IOException e) {
e.printStackTrace();
}
return getPortfolioId();
}
Deserializer.class
public class UserDeserializer implements JsonDeserializer<User> {
private Type listType = new TypeToken<List<Data>>(){}.getType();
#Override
public User deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
User user = new User();
JsonElement jsonElement;
if (json.isJsonArray()) {
jsonElement = json.getAsJsonArray();
user.data = context.deserialize(jsonElement,listType);
// user.data = new Gson().fromJson(jsonElement, new TypeToken<List<Data>>() {}.getType());
} else {
jsonElement = json.getAsJsonObject();
user.data = context.deserialize(jsonElement, Data.class);
// user.setData(new Gson().fromJson(jsonElement, new TypeToken<Data>() {}.getType()));
}
return user;
}
}
Gson builder in BaseApi class just in case:
Gson gson = new GsonBuilder().registerTypeAdapter(UserDeserializer.class, new UserDeserializer()).setLenient().create();
Without custom deserialization and Array JSON issue this would work perfectly. But now I have to determine "data" 's exact type I get.
In above case I get java.lang.ClassCastException: java.util.ArrayList cannot be cast to auto.Rest.Data
I assume I have to create another Data class (for example there will be DataObject & DataArray) and describe every parameter as I did before in Data.class to get this work? I think I do something wrong during deserialization but I'm not sure where tbh.
Or am I wrong and it is possible to invoke Data as List and Data as an Object of the same class?
I'm working on this for several days already(?) and was thinking about use generics instead of Gson help, yeah, I'm desperate. So any help appreciated.
if there is always one object, just add
json.getAsJsonArray().get(0);
public class UserDeserializer implements JsonDeserializer<User> {
private Type listType = new TypeToken<List<Data>>(){}.getType();
#Override
public User deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
User user = new User();
JsonElement jsonElement;
if (json.isJsonArray()) {
jsonElement = json.getAsJsonArray().get(0);
user.data = context.deserialize(jsonElement,listType);
// user.data = new Gson().fromJson(jsonElement, new TypeToken<List<Data>>() {}.getType());
} else {
jsonElement = json.getAsJsonObject();
user.data = context.deserialize(jsonElement, Data.class);
// user.setData(new Gson().fromJson(jsonElement, new TypeToken<Data>() {}.getType()));
}
return user;
}
}
if there are more objects, change field data to the list
public class UserDeserializer implements JsonDeserializer<User> {
private Type listType = new TypeToken<List<Data>>(){}.getType();
#Override
public User deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
User user = new User();
JsonElement jsonElement;
if (json.isJsonArray()) {
jsonElement = json.getAsJsonArray();
user.data = context.deserialize(jsonElement,listType);
// user.data = new Gson().fromJson(jsonElement, new TypeToken<List<Data>>() {}.getType());
} else {
jsonElement = json.getAsJsonObject();
List<Data> data = new ArrayList<Data>();
data.add(context.deserialize(jsonElement, Data.class)) ;
user.data = data ;
// user.setData(new Gson().fromJson(jsonElement, new TypeToken<Data>() {}.getType()));
}
return user;
}
}
and change User.class field data to List
public List<Data> data;
this is a similar topic in kotlin language link
If you always have object or one-element array you can write custom deserialiser as below:
class OneOrElementJsonDeserializer<T> implements JsonDeserializer<T> {
#Override
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json instanceof JsonArray) {
final JsonArray array = (JsonArray) json;
final int size = array.size();
if (size == 0) {
return null;
}
return context.deserialize(array.get(0), typeOfT);
}
return context.deserialize(json, typeOfT);
}
}
Your example model after simplification looks like below:
class User {
#JsonAdapter(OneOrElementJsonDeserializer.class)
private Data data;
public User() {
}
public User(Data data) {
super();
this.data = data;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
#Override
public String toString() {
return "User{" +
"data=" + data +
'}';
}
}
class Data {
private String id;
protected String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
#Override
public String toString() {
return "Data{" +
"id='" + id + '\'' +
'}';
}
}
Example usage:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Type;
public class GsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();
User root = gson.fromJson(new FileReader(jsonFile), User.class);
System.out.println(root);
}
}
Above code for below JSON payload:
{
"data": [
{
"id": "c87ca3fe85781007869b83f"
}
]
}
prints:
User{data=Data{id='c87ca3fe85781007869b83f'}}
And for object case JSON payload:
{
"data": {
"id": "c87ca3fe85781007869b83f"
}
}
prints:
User{data=Data{id='c87ca3fe85781007869b83f'}}
In case your property could contain JSON object or multi-element array see my answer to this question Mapping Json Array to Java Models. There is implemented deserialiser which handle cases like this.

How to serialize List object in Gson

I have data returned from DB using the below method (method from spark-java framework) below:
get("/data_on_page_load", "application/json", (Request request, Response response) -> {
List<Post> list = Post.findAll(); // NEED TO SERIALIZE THE RESPONSE
System.out.println("list is " + list);
return (list);
}, new JsonTransformer());
Data returned from DB:
[Model: com.soul.seeker.models.Post, table: 'post', attributes: {created_at=2017-03-26 04:06:35.0, details=aaa, id=36, title=Eventsa, url=eventsa, userImage=assets/img/spiritual-icon4.png, username=null}]
Where Post.findAll(); is method from http://javalite.io/record_selection#finding-all-records to get all records
Model: com.soul.seeker.models.Post is the POJO class below:
public class Post extends Model{
private String title;
private String details;
private String username;
private String userImage;
private String url;
private List categories;
//Getters and Setters removed for brevity
}
I am trying to serialize the out put using GSON TypeToken and TypeAdapter
ClassTypeAdapterFactory:
public class ClassTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if(!Class.class.isAssignableFrom(typeToken.getRawType())) {
return null;
}
return (TypeAdapter<T>) new ClassTypeAdapter();
}
}
ClassTypeAdapter:
public class ClassTypeAdapter extends TypeAdapter<Class<?>> {
#Override
public void write(JsonWriter jsonWriter, Class<?> clazz) throws IOException {
if(clazz == null){
jsonWriter.nullValue();
return;
}
jsonWriter.value(clazz.getName());
}
#Override
public Class<?> read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
return null;
}
Class<?> clazz = null;
try {
clazz = Class.forName(jsonReader.nextString());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return clazz;
}
}
Here I am using JsonTransformer which implements ResponseTransformer from spark-java ResponseTransformer interface
public class JsonTransformer implements ResponseTransformer {
private Gson gson = new Gson();
#Override
public String render(Object model) {
GsonBuilder gsonBuilder = new GsonBuilder();
gson = gsonBuilder.registerTypeAdapterFactory(new ClassTypeAdapterFactory()).create();
return gson.toJson(model);
}
}
Now the problem is the json out put also contains serialized class instead of only returning List object of pojo class. I am new to Java & Gson serialization.
Can any one please help in implementing TypeAdapter to return only List object? for Example like this:
Type listOfTestObject = new TypeToken<List<TestObject>>(){}.getType();
String s = gson.toJson(list, listOfTestObject);
List<TestObject> list2 = gson.fromJson(s, listOfTestObject);
I think that you won't need to type a Response Transformer, you can use a new Gson object directly like the last snippet at the response transformer documentation. It would end up like this:
get("/data_on_page_load", (req, res) -> {
res.type("application/json; charset=UTF-8");
List<Post> list = Post.findAll();
System.out.println("list is " + list);
return list;
}, new Gson()::toJson);

How to convert dynamic JSON reponse with Java Gson library

I have an API that can return JSON arrays or objects. Example JSON object
{
"id": 1,
"name": "name"
}
JSON array:
[
{
"id": 1,
"name": "name"
},
{
"id": 1,
"name": "name"
}
]
When mapping a JSON object response to a POJO I use:
MyEntity myEntity = new Gson().fromJson(jsonString, MyEntity.class);
When mapping a JSON array response to an array of POJOs I use:
MyEntity[] myEntity = new GSON().fromJson(jsonString, MyEntity[].class);
How can I convert those two responses to the appropriate types dynamically?
NOTE: I can't modify the server response, this is a public API.
Thank you!
EDIT:
I am trying to implement a method that does this automatically but I am missing something. The method
public <T> T convertResponseToEntity(Class<T> classOfT)
{
JsonElement jsonElement = this.gson.fromJson(getResponseAsString(), JsonElement.class);
if (jsonElement.isJsonArray()) {
Type listType = new TypeToken<T>(){}.getType();
return this.gson.fromJson(getResponseAsString(), listType);
}
return this.gson.fromJson(getResponseAsString(), (Type) classOfT);
}
It returns a list of LinkedTreeMaps. How can I modify the code to return the same content as Object[]?
How can I convert those 2 responses dynamically to the appropriate type?
It depends on how to interpret the "appropriate type" here because it would lead to instanceof or visitor pattern to get the appropriate type once you try to handle the parsed-from-JSON object every time you need it. If you can't change the API, you can smooth the way you use it. One of possible options here is handling such response as if everything is a list. Even a single object can be handled as a list with one element only (and many libraries work with sequences/lists only having that fact: Stream API in Java, LINQ in .NET, jQuery in JavaScript, etc).
Suppose you have the following MyEntity class to handle the elements obtained from the API you need:
// For the testing purposes, package-visible final fields are perfect
// Gson can deal with final fields too
final class MyEntity {
final int id = Integer.valueOf(0); // not letting javac to inline 0 since it's primitive
final String name = null;
#Override
public String toString() {
return id + "=>" + name;
}
}
Next, let's create a type adapter that will always align "true" lists and single objects as if it were a list:
final class AlwaysListTypeAdapter<T>
extends TypeAdapter<List<T>> {
private final TypeAdapter<T> elementTypeAdapter;
private AlwaysListTypeAdapter(final TypeAdapter<T> elementTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
}
static <T> TypeAdapter<List<T>> getAlwaysListTypeAdapter(final TypeAdapter<T> elementTypeAdapter) {
return new AlwaysListTypeAdapter<>(elementTypeAdapter);
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final List<T> list)
throws IOException {
if ( list == null ) {
out.nullValue();
} else {
switch ( list.size() ) {
case 0:
out.beginArray();
out.endArray();
break;
case 1:
elementTypeAdapter.write(out, list.iterator().next());
break;
default:
out.beginArray();
for ( final T element : list ) {
elementTypeAdapter.write(out, element);
}
out.endArray();
break;
}
}
}
#Override
public List<T> read(final JsonReader in)
throws IOException {
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
final List<T> list = new ArrayList<>();
in.beginArray();
while ( in.peek() != END_ARRAY ) {
list.add(elementTypeAdapter.read(in));
}
in.endArray();
return unmodifiableList(list);
case BEGIN_OBJECT:
return singletonList(elementTypeAdapter.read(in));
case NULL:
return null;
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case BOOLEAN:
case END_DOCUMENT:
throw new MalformedJsonException("Unexpected token: " + token);
default:
// A guard case: what if Gson would add another token someday?
throw new AssertionError("Must never happen: " + token);
}
}
}
Gson TypeAdapter are designed to work in streaming fashion thus they are cheap from the efficiency perspective, but not that easy in implementation. The write() method above is implemented just for the sake of not putting throw new UnsupportedOperationException(); there (I'm assuming you only read that API, but don't know if that API might consume "either element or a list" modification requests). Now it's necessary to create a type adapter factory to let Gson pick up the right type adapter for every particular type:
final class AlwaysListTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory alwaysListTypeAdapterFactory = new AlwaysListTypeAdapterFactory();
private AlwaysListTypeAdapterFactory() {
}
static TypeAdapterFactory getAlwaysListTypeAdapterFactory() {
return alwaysListTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken)
throws IllegalArgumentException {
if ( List.class.isAssignableFrom(typeToken.getRawType()) ) {
final Type elementType = getElementType(typeToken);
// Class<T> instances can be compared with ==
final TypeAdapter<?> elementTypeAdapter = elementType == MyEntity.class ? gson.getAdapter(MyEntity.class) : null;
// Found supported element type adapter?
if ( elementTypeAdapter != null ) {
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) getAlwaysListTypeAdapter(elementTypeAdapter);
return castTypeAdapter;
}
}
// Not a type that can be handled? Let Gson pick a more appropriate one itself
return null;
}
// Attempt to detect the list element type
private static Type getElementType(final TypeToken<?> typeToken) {
final Type listType = typeToken.getType();
return listType instanceof ParameterizedType
? ((ParameterizedType) listType).getActualTypeArguments()[0]
: Object.class;
}
}
And how it's used after all:
private static final Type responseItemListType = new TypeToken<List<MyEntity>>() {
}.getType();
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getAlwaysListTypeAdapterFactory())
.create();
public static void main(final String... args) {
test("");
test("{\"id\":1,\"name\":\"name\"}");
test("[{\"id\":1,\"name\":\"name\"},{\"id\":1,\"name\":\"name\"}]");
test("[]");
}
private static void test(final String incomingJson) {
final List<MyEntity> list = gson.fromJson(incomingJson, responseItemListType);
System.out.print("LIST=");
System.out.println(list);
System.out.print("JSON=");
gson.toJson(list, responseItemListType, System.out); // no need to create an intermediate string, let it just stream
System.out.println();
System.out.println("-----------------------------------");
}
The output:
LIST=null
JSON=null
-----------------------------------
LIST=[1=>name]
JSON={"id":1,"name":"name"}
-----------------------------------
LIST=[1=>name, 1=>name]
JSON=[{"id":1,"name":"name"},{"id":1,"name":"name"}]
-----------------------------------
LIST=[]
JSON=[]
-----------------------------------
Just parse it into JsonElement and check actual element type:
Gson g = new Gson();
JsonParser parser = new JsonParser();
JsonElement e = parser.parse( new StringReader(jsonString) );
if(e instanceof JsonObject) {
MyEntity myEntity = g.fromJson(e, MyEntity.class);
} else {
MyEntity[] myEntity = g.fromJson(e, MyEntity[].class);
}

How to do conditional Gson deserialization default value

Imagine if I have the following JSON
{"game":"football", "people":"elevent"}
{"game":"badminton", "people":"two"}
My class as below
class Sport {
String game;
String people;
}
I could do a deserialize of my Json as below
Sport mySport = Gson().fromJson(json, Sport.class);
However, if my JSON is only
{"game":"football"}
{"game":"badminton"}
I would like it to automatically initialize people to "elevent" or "two", pending of the first field. Is there a way to configure my GsonBuilder() to have that achieve automatically during deserialization?
You could create a custom JsonDeserializer:
public class SportDeserializer implements JsonDeserializer<Sport> {
#Override
public Sport deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = (JsonObject) json;
String game = jsonObject.get("game").getAsString();
JsonElement nullablePeople = jsonObject.get("people");
String people = nullablePeople == null ? null : nullablePeople.getAsString();
if (people == null || people.isEmpty()) {
if (game.equals("football")) {
people = "elevent";
}
else if (game.equals("badminton")) {
people = "two";
}
}
Sport sport = new Sport();
sport.game = game;
sport.people = people;
return sport;
}
}
And then use the custom JsonDeserializer:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Sport.class, new SportDeserializer());
Gson gson = gsonBuilder.create();
Sport sport = gson.fromJson(jsonString, Sport.class);
My answer below isn't the best for this question since I simplified the question, and the other answer would suite this better.
But for more complicated scenarios, my answer below would help. It is basically setup a post-processing after the GSon converted.
I finally use Gson convert post-processing.
class Sport implements PostProcessingEnabler.PostProcessable {
String game;
String people;
#Override
public void gsonPostProcess() {
// The below is something simple.
// Could have more complicated scneario.
if (game.equals("football")) {
people = "elevant";
} else if (game.equals("badminton")) {
people = "two";
}
}
}
class PostProcessingEnabler implements TypeAdapterFactory {
public interface PostProcessable {
void gsonPostProcess();
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
T obj = delegate.read(in);
if (obj instanceof PostProcessable) {
((PostProcessable) obj).gsonPostProcess();
}
return obj;
}
};
}
}
Tribute goes to https://blog.simplypatrick.com/til/2016/2016-03-02-post-processing-GSON-deserialization/

Gson to parse generic type, when type is known from json

I have jsons that contain a message of any type and the json contains a String that says which type the message has.
I want to deserialize them and get 1. an Instance of the messag type representing the message and 2. an instance of Topic where T is the message type.
As examples:
input1
{
"messageType":"String",
"message": "a string"
}
I expect a result after the deserialization to be the same as this done by hand:
Topic<String> t = new Topic<String>(String.class);
String message = "a string";
input2
{
"messageType":"Integer",
"message": 1
}
I expect a result after the deserialization to be the same as this done by hand:
Topic<Integer> t = new Topic<Integer>(Integer.class);
Integer message = 1;
input3
{
"messageType":"MyClass",
"message": {"a": "something", "b": 1}
}
I expect a result after the deserialization to be the same as this done by hand:
Topic<MyClass> t = new Topic<MyClass>(MyClass.class);
MyClass message = new MyClass("something", 1);
input4
... same with other types ...
I think you got the point. But now I need to do this somehow in a generic/abstract way. I tried this, but this will not work:
private enum MessageType {
STRING(String.class), INTEGER(Integer.class), BOOLEAN(Boolean.class), MYCLASS(MyClass.class);
private Class<?> clazz;
MessageType(Class<?> clazz) {
this.clazz = clazz;
}
}
private static class MyJson {
String topicId;
String messageType;
Object message;
}
MyJson<?> myJson = gson.fromJson(input, MyJson.class);
MessageType type = MessageType.valueOf(myJson.messageType);
Class<?> clazz = type.getClass();
??? message = clazz.newInstance(message);
Topic<???> t = new ?????
I don't know what to do?! I need Topic and Message typed, but how??? The following seems so bad:
#SuppressWarnings("unchecked")
private <T> Topic<T> createTopic(Class<T> typeClass) {
try {
return Topic.class.getConstructor(typeClass).newInstance(typeClass);
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Fail");
}
}
That works for me now:
class Topic<MessageType> {
// ...
public Class<MessageType> getMessageTypeClass() // ...
// ...
}
parse result data struct:
public class TopicLine {
public Topic<?> topic;
public Object message;
}
deserializer:
public class MessageDeserializer implements JsonDeserializer<TopicLine> {
private static class InternalParseLine {
String messageType;
JsonElement message;
}
private Map<String, Topic<?>> messageTypes = new HashMap<String, Topic<?>>();
public MessageDeserializer() {
messageTypes.put("Integer", new Topic<Integer>(Integer.class));
// other topics
}
private static class InternalParseLine {
String topicId;
JsonElement message;
}
#Override
public TopicLine deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
InternalParseLine line = context.deserialize(json, InternalParseLine.class);
TopicLine topicLine = new TopicLine();
topicLine.topic = topics.get(line.messageType);
topicLine.message = context.deserialize(line.message, topicLine.topic.getMessageTypeClass());
return topicLine;
}
}
usage:
json = new GsonBuilder()
.registerTypeAdapter(TopicLine.class, new TopicLineDeserializer())
.create();
TopicLine t = json.fromJson(line, TopicLine.class);
In fact, to make it more dynamic I use a topics-manager which I pass to the deserializer in its constructor instead of a static map. So I can register and unregister topic/message-types on the fly during runtime.

Categories

Resources