My code gets the response of an HTTP call and converts the string to a Json object.
return gson.fromJson( String.valueOf(execute( requestInfo.getHttpRequest() )), requestInfo.getResponseType() );
However, the gson.fromJson method, requires you to specify the object to which you want to convert it to.
I do not want any response class for this particular call, and would like to return void/null. However, since this is a shared method across multiple methods, I have to pass in some class. At the moment, I am passing in this,
private static final Type RETURN_TYPE = new TypeToken<NameOfMyResponseClass>() {
}.getType();
How can I replace this will null or void, so that nothing is returned?
PS : The reason I want to pass null is that, the response for this method contains a ton of members, and I do not want to create a POJO having all these members initialized so I can convert to a gson object, which is not even useful for me.
Ideally, if there was something like, that's what I am looking for.
private static final Type RETURN_TYPE = new TypeToken<Void>() {
}.getType();
It looks like you simply use:
private static <T> T fromJson(final String json, final Type type) {
if ( type == null ) {
return null;
}
return gson.fromJson(json, type);
}
If, for some justified reason, you cannot pass the null to the fromJson method, you can create a Void and void-friendly type adapter and bind it to your Gson instance (of course, you cannot return a void "value"):
final class VoidTypeAdapter
extends TypeAdapter<Void> {
private static final TypeAdapter<Void> voidTypeAdapter = new VoidTypeAdapter();
private VoidTypeAdapter() {
}
static TypeAdapter<Void> getVoidTypeAdapter() {
return voidTypeAdapter;
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final Void value)
throws IOException {
out.nullValue();
}
#Override
public Void read(final JsonReader in)
throws IOException {
// Skip the current JSON tokens stream value entirely
in.skipValue();
return null;
}
}
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(Void.class, getVoidTypeAdapter())
.registerTypeAdapter(void.class, getVoidTypeAdapter())
.create();
private static <T> T fromJson(final String json, final Type type) {
return gson.fromJson(json, type);
}
private static String toJson(final Object object, final Type type) {
return gson.toJson(object, type);
}
So a simple test might look like this:
private static void test(final Type type) {
System.out.println(type);
final Object value = fromJson("[\"foo\",\"bar\"]", type);
System.out.println("-\t" + value);
System.out.println("-\t" + toJson(value, type));
}
public static void main(final String... args) {
test(new TypeToken<List<String>>() {}.getType());
test(Void.class);
test(void.class);
}
Output:
java.util.List
- [foo, bar]
- ["foo","bar"]
class java.lang.Void
- null
- null
void
- null
- null
Note that type tokens are mostly used to build a type information for generic types. In more simple cases you can use .class to get Class<?>: int.class, Integer.class, void.class, Void.class, int[][][][][].class, etc.
Related
Here is the json schema:
As you can see, rated can be both boolean and object.
I am using Retrofit 2 and Gson converter. How should I create my model for this schema?
Here's how I solved this issue:
Create a custom type adapter in your model and parse rated manually;
public class AccountState {
//#SerializedName("rated") //NOPE, parse it manually
private Integer mRated; //also don't name it rated
public Integer getRated() {
return mRated;
}
public void setRated(Integer rated) {
this.mRated = rated;
}
public static class AccountStateDeserializer implements JsonDeserializer<AccountState> {
#Override
public AccountState deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
AccountState accountState = new Gson().fromJson(json, AccountState.class);
JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("rated")) {
JsonElement elem = jsonObject.get("rated");
if (elem != null && !elem.isJsonNull()) {
if(elem.isJsonPrimitive()){
accountState.setRated(null);
}else{
accountState.setRated(elem.getAsJsonObject().get("value").getAsInt());
}
}
}
return accountState ;
}
}
}
Here you create your gson with this custom adapter:
final static Gson gson = new GsonBuilder()
.registerTypeAdapter(AccountState.class, new AccountState.AccountStateDeserializer())
.create();
Add it to retrofit like that:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.ENDPOINT)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build();
TADADADADADADADDAD!
You can make it work without having to implement a custom converter.
All you have to do is put a general "Object" type for the variable and then you just check which data type it is by doing this:
if(object.getClass == YourClass.class){
Whatever we = ((YourClass) object).getWhatever();
} else if(object.getClass == YourOtherClass.class){
String name = ((YourOtherClass) object).getName();
}
You can add as many data types to this variable as you like.
You can also use the java types "String.class", "Boolean.class" or whatever you like.
Gson has a nice feature allowing to inject a custom type adapter or a type adapter factory to a certain field therefore letting Gson to manage the host object and the latter's fields (de)serialization. So, you can be sure that AccountState could be still deserialized with ReflectiveTypeAdapterFactory and ReflectiveTypeAdapterFactory.Adapter so all deserialization strategies defined in GsonBuilder could be applied.
final class AccountState {
// This is what can make life easier. Note its advantages:
// * PackedBooleanTypeAdapterFactory can be reused multiple times
// * AccountState life-cycle can be managed by Gson itself,
// so it can manage *very* complex deserialization automatically.
#JsonAdapter(PackedBooleanTypeAdapterFactory.class)
final Boolean rated = null;
}
Next, how PackageBooleanTypeAdapterFactory is implemented:
final class PackedBooleanTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate this itself, no need to expose
private PackedBooleanTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if it's the type we can handle ourself
if ( typeToken.getRawType() == Boolean.class ) {
final TypeAdapter<Boolean> typeAdapter = new PackedIntegerTypeAdapter(gson);
// Some Java "unchecked" boilerplate here...
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
// If it's something else, let Gson pick a downstream type adapter on its own
return null;
}
private static final class PackedIntegerTypeAdapter
extends TypeAdapter<Boolean> {
private final Gson gson;
private PackedIntegerTypeAdapter(final Gson gson) {
this.gson = gson;
}
#Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
#Override
public Boolean read(final JsonReader in)
throws MalformedJsonException {
// Pick next token as a JsonElement
final JsonElement jsonElement = gson.fromJson(in, JsonElement.class);
// Note that Gson uses JsonNull singleton to denote a null
if ( jsonElement.isJsonNull() ) {
return null;
}
if ( jsonElement.isJsonPrimitive() ) {
return jsonElement
.getAsJsonPrimitive()
.getAsBoolean();
}
if ( jsonElement.isJsonObject() ) {
return jsonElement
.getAsJsonObject()
.getAsJsonPrimitive("value")
.getAsBoolean();
}
// Not something we can handle
throw new MalformedJsonException("Cannot parse: " + jsonElement);
}
}
}
Demo:
public static void main(final String... args) {
parseAndDump("{\"rated\":null}");
parseAndDump("{\"rated\":true}");
parseAndDump("{\"rated\":{\"value\":true}}");
}
private static void parseAndDump(final String json) {
final AccountState accountState = gson.fromJson(json, AccountState.class);
System.out.println(accountState.rated);
}
Output:
null
true
true
Note that JsonSerializer and JsonDeserializer both have some performance and memory cost due to its tree model design (you can traverse JSON trees easily as long as they are in memory). Sometimes, for simple cases, a streaming type adapter may be preferable. Pros: consumes less memory and works faster. Cons: hard to implement.
final class AccountState {
#JsonAdapter(PackedBooleanTypeAdapter.class)
final Boolean rated = null;
}
Note that the rated field accepts a type adapter directly because it does not need Gson instances to build JSON trees (JsonElements).
final class PackedBooleanTypeAdapter
extends TypeAdapter<Boolean> {
// Gson still can instantiate this type adapter itself
private PackedBooleanTypeAdapter() {
}
#Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
#Override
public Boolean read(final JsonReader in)
throws IOException {
// Peeking the next JSON token and dispatching parsing according to the given token
final JsonToken token = in.peek();
switch ( token ) {
case NULL:
return parseAsNull(in);
case BOOLEAN:
return parseAsBoolean(in);
case BEGIN_OBJECT:
return parseAsObject(in);
// The below might be omitted, since some code styles prefer all switch/enum constants explicitly
case BEGIN_ARRAY:
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case END_DOCUMENT:
throw new MalformedJsonException("Cannot parse: " + token);
// Not a known token, and must never happen -- something new in a newer Gson version?
default:
throw new AssertionError(token);
}
}
private Boolean parseAsNull(final JsonReader in)
throws IOException {
// null token still has to be consumed from the reader
in.nextNull();
return null;
}
private Boolean parseAsBoolean(final JsonReader in)
throws IOException {
// Consume a boolean value from the reader
return in.nextBoolean();
}
private Boolean parseAsObject(final JsonReader in)
throws IOException {
// Consume the begin object token `{`
in.beginObject();
// Get the next property name
final String property = in.nextName();
// Not a value? Then probably it's not what we're expecting for
if ( !property.equals("value") ) {
throw new MalformedJsonException("Unexpected property: " + property);
}
// Assuming the property "value" value must be a boolean
final boolean value = in.nextBoolean();
// Consume the object end token `}`
in.endObject();
return value;
}
}
This one should work faster. The output remains the same. Note that Gson does not require a GsonBuilder for both cases. As far as I remember how Retrofit 2 works, GsonConverterFactory is still required (not sure, Gson is not the default serializer in Retrofit 2?).
I want to create a generic method to return deserialized data. The idea is to pass through parameter a Type.class and when it deserialize using Gson, it returns a collection or a single object from Type.
For example:
public class Client {
String id;
String name;
/* getters and setters */
}
public class Account {
String number;
String bank;
/* getters and setters */
}
public class Main {
public static void main(String[] args) {
List<Client> clients = Utils.getList(Client.class, "");
Account account = Utils.getSingle(Account.class, "number = '23145'");
}
}
public class Utils {
public static Class<? extends Collection> getList(Class<?> type, String query) {
//select and stuff, works fine and returns a Map<String, Object> called map, for example
Gson gson = new Gson();
JsonElement element = gson.fromJsonTree(map);
//Here's the problem. How to return a collection of type or a single type?
return gson.fromJson(element, type);
}
public static Class<?> getSingle(Class<?> type, String query) {
//select and stuff, works fine and returns a Map<String, Object> called map, for example
Gson gson = new Gson();
JsonElement element = gson.fromJsonTree(map);
//Here's the problem. How to return a collection of type or a single type?
return gson.fromJson(element, type);
}
}
How can I return a single object from Type or a list of it?
First of all you need change method signature to generic type:
public static <T> List<T> getList(Class<T> type, String query)
and
public static <T> T getSingle(Class<T> type, String query)
getSingle method should start working, but for method getList you need to change implementation:
create Type listType = new TypeToken<List<T>>() {}.getType();
return gson.fromJson(element, listType);
you can find more information about com.google.gson.Gson from javaDoc
I have bunch of model classes which have fields of type List<X> where X is one of many things (e.g. String, Integer, but also some of my own types). I'm using GSON to parse JSON representations of these models.
My problem is that the server I'm dealing with (which is beyond my control) somehow removed singleton arrays and replaces them by the contained object.
For example, instead of returning:
{
"foo": [ "bar"],
"bleh": [ { "some": "object" } ]
}
It returns:
{
"foo": "bar",
"bleh": { "some": "object" }
}
Now assume that the Java model class look something like this:
public class Model {
private List<String> foo;
private List<SomeObject> bleh;
}
Currently this causes GSON to throw an exception because it finds BEGIN_STRING or BEGIN_OBJECT where it expects BEGIN_ARRAY.
For arrays or lists of Strings this is easily solved using a TypeAdapter<List<String>>. But the problem is I have Lists with many different element types and I don't want to write a separate TypeAdapter for each case. Nor have I been able to a generic TypeAdapter<List<?>>, because at some point you need to know the type.
So is there another way to configure GSON to be smart enough to turn single objects or values into arrays/lists? Or in other words, just "pretend" that the [ and ] are there where it expects to find them although they aren't there?
But the problem is I have Lists with many different element types and I don't want to write a separate TypeAdapter for each case. Nor have I been able to a generic TypeAdapter>, because at some point you need to know the type.
This is what type adapter factories are designed for: you can control every type in Gson instance configuration.
final class AlwaysListTypeAdapterFactory<E>
implements TypeAdapterFactory {
// Gson can instantiate it itself
private AlwaysListTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// If it's not a List -- just delegate the job to Gson and let it pick the best type adapter itself
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
// Resolving the list parameter type
final Type elementType = resolveTypeArgument(typeToken.getType());
#SuppressWarnings("unchecked")
final TypeAdapter<E> elementTypeAdapter = (TypeAdapter<E>) gson.getAdapter(TypeToken.get(elementType));
// Note that the always-list type adapter is made null-safe, so we don't have to check nulls ourselves
#SuppressWarnings("unchecked")
final TypeAdapter<T> alwaysListTypeAdapter = (TypeAdapter<T>) new AlwaysListTypeAdapter<>(elementTypeAdapter).nullSafe();
return alwaysListTypeAdapter;
}
private static Type resolveTypeArgument(final Type type) {
// The given type is not parameterized?
if ( !(type instanceof ParameterizedType) ) {
// No, raw
return Object.class;
}
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[0];
}
private static final class AlwaysListTypeAdapter<E>
extends TypeAdapter<List<E>> {
private final TypeAdapter<E> elementTypeAdapter;
private AlwaysListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<E> list) {
throw new UnsupportedOperationException();
}
#Override
public List<E> read(final JsonReader in)
throws IOException {
// This is where we detect the list "type"
final List<E> list = new ArrayList<>();
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
// If it's a regular list, just consume [, <all elements>, and ]
in.beginArray();
while ( in.hasNext() ) {
list.add(elementTypeAdapter.read(in));
}
in.endArray();
break;
case BEGIN_OBJECT:
case STRING:
case NUMBER:
case BOOLEAN:
// An object or a primitive? Just add the current value to the result list
list.add(elementTypeAdapter.read(in));
break;
case NULL:
throw new AssertionError("Must never happen: check if the type adapter configured with .nullSafe()");
case NAME:
case END_ARRAY:
case END_OBJECT:
case END_DOCUMENT:
throw new MalformedJsonException("Unexpected token: " + token);
default:
throw new AssertionError("Must never happen: " + token);
}
return list;
}
}
}
Now you just have to tell Gson which fields are not well-formed.
Of course, you might configure the whole Gson instance to accept such lists, but let it be more precise using the #JsonAdapter annotation:
final class Model {
#JsonAdapter(AlwaysListTypeAdapterFactory.class)
final List<String> foo = null;
#JsonAdapter(AlwaysListTypeAdapterFactory.class)
final List<SomeObject> bleh = null;
#Override
public String toString() {
return "Model{" + "foo=" + foo + ", bleh=" + bleh + '}';
}
}
final class SomeObject {
final String some = null;
#Override
public String toString() {
return "SomeObject{" + "some='" + some + '\'' + '}';
}
}
Test data:
single.json
{
"foo": "bar",
"bleh": {"some": "object"}
}
list.json
{
"foo": ["bar"],
"bleh": [{"some": "object"}]
}
Example:
private static final Gson gson = new Gson();
public static void main(final String... args)
throws IOException {
for ( final String resource : ImmutableList.of("single.json", "list.json") ) {
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43412261.class, resource) ) {
final Model model = gson.fromJson(jsonReader, Model.class);
System.out.println(model);
}
}
}
And the output:
Model{foo=[bar], bleh=[SomeObject{some='object'}]}
Model{foo=[bar], bleh=[SomeObject{some='object'}]}
You can simply write your own JsonDeserializer where you check whether your bleh or foo are JsonObjects or JsonArrays.
To check if a JsonElement is an array or an object:
JsonElement element = ...;
if (element.isJsonObject()) {
//element is a JsonObject
} else if (element.isJsonArray()) {
//element is a JsonArray
}
One solution to this would be to write a custom TypeAdapterFactory which creates an adapter which peeks at the JSON data. If it encounters something other than a JSON array (or JSON null) it wraps it inside a JSON array before deserializing it:
// Only intended for usage with #JsonAdapter on fields
class SingleValueOrListAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Note: Cannot use getDelegateAdapter due to https://github.com/google/gson/issues/1028
TypeAdapter<T> listAdapterDelegate = gson.getAdapter(type);
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
listAdapterDelegate.write(out, value);
}
#Override
public T read(JsonReader in) throws IOException {
JsonToken peeked = in.peek();
if (peeked == JsonToken.NULL || peeked == JsonToken.BEGIN_ARRAY) {
return listAdapterDelegate.read(in);
} else {
// Wrap JSON element in a new JSON array before deserializing it
JsonElement jsonElement = jsonElementAdapter.read(in);
JsonArray jsonArray = new JsonArray();
jsonArray.add(jsonElement);
return listAdapterDelegate.fromJsonTree(jsonArray);
}
}
};
}
}
The above implementation is designed only for usage with #JsonAdapter on fields, for example:
#JsonAdapter(SingleValueOrListAdapterFactory.class)
private List<MyClass> myField;
Compared to the currently accepted answer this provides the following advantages because it simply delegates the actual deserialization to listAdapterDelegate:
Custom List (or Collection) subclasses are supported because creation of them is delegated to Gson
Gson's default type resolution logic is used to determine the element type and to deserialize it
But it also has the following disadvantage:
Decreased performance because if the data is not already in a JSON array it is first deserialized to a JsonElement before the actual deserialization is performed
When using the GSON library, you could just check whether or not the following token is an object or an array. This of course requires you to go more fine grained while parsing the XML, but it allows you full control of what do you want to get from it. Sometimes we are not under control of the XML, and it could come handy.
This is an example to check if the next token is an object or an array, using the JsonReader class to parse the file:
if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
jsonReader.beginArray()
} else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
jsonReader.beginObject()
}
And at the end of the array / object, you could do the same, but for the end tokens:
if (jsonReader.peek() == JsonToken.END_ARRAY) {
jsonReader.endArray()
} else if (jsonReader.peek() == JsonToken.END_OBJECT) {
jsonReader.endObject()
}
This way, you could have identical code (adding an extra check, to verify if you are on an array or on an object) to parse your array of objects, or a single object.
I had this same problem consuming xml / json from a vendor - they certainly weren't going to change their code for me :) There were several resources on the web that I used before changing adapting them to my own version This SO answer was very helpful. I spent some time looking at the gson code and finding a lot of private variable that I wanted access to. So, essentially what my custom collection adapter does is peek to see if the next element is an object. If not, we just delegate the read to the previous adapter (that we have overridden).
If the next element is an object, we use gson to process that. We then convert that to an array of one object. Use gson to write that to a string, then pass that string as a JsonReader to the underlying adapter. This can then create an instance of the underlying list and add the one element we have.
Here's the AdapterTypeFactory:
public enum ListSingleObjectAdapterFactory implements TypeAdapterFactory {
INSTANCE; // Josh Bloch's Enum singleton pattern
#SuppressWarnings({ "unchecked", "rawtypes" })
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
if (!Collection.class.isAssignableFrom(rawType)) {
return null;
}
TypeAdapter collectionAdapter = gson.getDelegateAdapter(this, typeToken);
Class genericClass = (Class) ((ParameterizedType) typeToken.getType())
.getActualTypeArguments()[0];
return new SingleObjectOrCollectionAdapter(
gson, collectionAdapter, genericClass);
}
}
Then the type adapter I have is:
public class SingleObjectOrCollectionAdapter<T> extends TypeAdapter<Collection<T>> {
private Class<T> adapterclass;
private Gson gson;
private TypeAdapter arrayTypeAdapter;
public SingleObjectOrCollectionAdapter(Gson gson, TypeAdapter<T> collectionTypeAdapter, Class<T> componentType) {
arrayTypeAdapter = collectionTypeAdapter;
this.gson = gson;
adapterclass = componentType;
}
#Override
public Collection<T> read(JsonReader reader) throws IOException {
Collection<T> collection;
JsonReader myReader = reader;
if (reader.peek() == JsonToken.BEGIN_OBJECT) {
T inning = gson.fromJson(reader, adapterclass);
String s = gson.toJson(new Object[]{inning});
myReader = new JsonReader(new StringReader(s));
}
collection = (Collection)arrayTypeAdapter.read( myReader );
return collection;
}
#Override
public void write(JsonWriter writer, Collection<T> value) throws IOException {
arrayTypeAdapter.write(writer, value);
}
}
Finally, we need to register the adapter factory:
GsonBuilder gb = new GsonBuilder().registerTypeAdapterFactory(ListSingleObjectAdapterFactory.INSTANCE);
So far, it seems to be working well handling both single and multiple objects - although I wouldn't be surprised if it needs some tweaking down the road.
If I have a class with a generic like this:
public class ResponseSimple<T> {
private Map<String, Collection<String>> headers;
private int status;
private T body;
}
Then,in other class I have a method which I need to use an instance of this class, but the method passes by param a java.lang.reflect.Type and it's overrided so I can't change the any of the method (name, signature..):
public class ResponseEncoder extends GsonDecoder {
public ResponseEncoder() {
super();
}
#Override
public Object decode(Response response, Type type) throws IOException
{
//How assign type T using type param??
//¿ResponseSimple<T> responseSimple = new ResponseSimple();?
return null;
}
}
How could I assign the generic type T using the param type (java.lang.reflect.Type)?
I would suggest something like this:
#Override
public <T> T decode(Response response, Class<T> type) throws IOException
{
//How assign type T using type param??
ResponseSimple<T> response = new ResponseSimple<T>();
return response;
}
Then use decode as follows:
.decode(response, NameOfClass.class)
Edit:
If you need to extend your class you could use a static helper function:
public static <T> ResponseSimple<T> createResponse(Class<T> clazz)
{
return new ResponseSimple<>();
}
And use it like this:
public class ResponseEncoder extends GsonDecoder {
public ResponseEncoder() {
super();
}
#Override
public Object decode(Response response, Type type) throws IOException
{
Class<?> clazz = (Class<?>) type;
ResponseSimple<?> response = createResonse(clazz);
return null;
}
}
I hope I understood your question correctly.
To create a new instance of your generic class you need to infer the correct type arguments like this if you want your ResponseSimple<T> to contain java.lang.reflect.Type:
ResponseSimple<Type> response = new ResponseSimple<>();
So, inbetween the <> you need to add the name of the class you want to use.
Also have a look at this: https://docs.oracle.com/javase/tutorial/java/generics/types.html
//EDIT:
As you said you want to infer the type arguments dynamically, what you did works fine for me. The only thing is that you forgot the diamond operator:
#Override
public Object decode(Response response, T type) throws IOException
{
ResponseSimple<T> response = new ResponseSimple<>(); //<> in the second part
return null;
}
I have never done much with serialization, but am trying to use Google's gson to serialize a Java object to a file. Here is an example of my issue:
public interface Animal {
public String getName();
}
public class Cat implements Animal {
private String mName = "Cat";
private String mHabbit = "Playing with yarn";
public String getName() {
return mName;
}
public void setName(String pName) {
mName = pName;
}
public String getHabbit() {
return mHabbit;
}
public void setHabbit(String pHabbit) {
mHabbit = pHabbit;
}
}
public class Exhibit {
private String mDescription;
private Animal mAnimal;
public Exhibit() {
mDescription = "This is a public exhibit.";
}
public String getDescription() {
return mDescription;
}
public void setDescription(String pDescription) {
mDescription = pDescription;
}
public Animal getAnimal() {
return mAnimal;
}
public void setAnimal(Animal pAnimal) {
mAnimal = pAnimal;
}
}
public class GsonTest {
public static void main(String[] argv) {
Exhibit exhibit = new Exhibit();
exhibit.setAnimal(new Cat());
Gson gson = new Gson();
String jsonString = gson.toJson(exhibit);
System.out.println(jsonString);
Exhibit deserializedExhibit = gson.fromJson(jsonString, Exhibit.class);
System.out.println(deserializedExhibit);
}
}
So this serializes nicely -- but understandably drops the type information on the Animal:
{"mDescription":"This is a public exhibit.","mAnimal":{"mName":"Cat","mHabbit":"Playing with yarn"}}
This causes real problems for deserialization, though:
Exception in thread "main" java.lang.RuntimeException: No-args constructor for interface com.atg.lp.gson.Animal does not exist. Register an InstanceCreator with Gson for this type to fix this problem.
I get why this is happening, but am having trouble figuring out the proper pattern for dealing with this. I did look in the guide but it didn't address this directly.
Here is a generic solution that works for all cases where only interface is known statically.
Create serialiser/deserialiser:
final class InterfaceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> {
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
final JsonObject wrapper = new JsonObject();
wrapper.addProperty("type", object.getClass().getName());
wrapper.add("data", context.serialize(object));
return wrapper;
}
public T deserialize(JsonElement elem, Type interfaceType, JsonDeserializationContext context) throws JsonParseException {
final JsonObject wrapper = (JsonObject) elem;
final JsonElement typeName = get(wrapper, "type");
final JsonElement data = get(wrapper, "data");
final Type actualType = typeForName(typeName);
return context.deserialize(data, actualType);
}
private Type typeForName(final JsonElement typeElem) {
try {
return Class.forName(typeElem.getAsString());
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
private JsonElement get(final JsonObject wrapper, String memberName) {
final JsonElement elem = wrapper.get(memberName);
if (elem == null) throw new JsonParseException("no '" + memberName + "' member found in what was expected to be an interface wrapper");
return elem;
}
}
make Gson use it for the interface type of your choice:
Gson gson = new GsonBuilder().registerTypeAdapter(Animal.class, new InterfaceAdapter<Animal>())
.create();
Put the animal as transient, it will then not be serialized.
Or you can serialize it yourself by implementing defaultWriteObject(...) and defaultReadObject(...) (I think thats what they were called...)
EDIT See the part about "Writing an Instance Creator" here.
Gson cant deserialize an interface since it doesnt know which implementing class will be used, so you need to provide an instance creator for your Animal and set a default or similar.
#Maciek solution works perfect if the declared type of the member variable is the interface / abstract class. It won't work if the declared type is sub-class / sub-interface / sub-abstract class unless we register them all through registerTypeAdapter(). We can avoid registering one by one with the use of registerTypeHierarchyAdapter, but I realize that it will cause StackOverflowError because of the infinite loop. (Please read reference section below)
In short, my workaround solution looks a bit senseless but it works without StackOverflowError.
#Override
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
final JsonObject wrapper = new JsonObject();
wrapper.addProperty("type", object.getClass().getName());
wrapper.add("data", new Gson().toJsonTree(object));
return wrapper;
}
I used another new Gson instance of work as the default serializer / deserializer to avoid infinite loop. The drawback of this solution is you will also lose other TypeAdapter as well, if you have custom serialization for another type and it appears in the object, it will simply fail.
Still, I am hoping for a better solution.
Reference
According to Gson 2.3.1 documentation for JsonSerializationContext and JsonDeserializationContext
Invokes default serialization on the specified object passing the specific type information. It should never be invoked on the element received as a parameter of the JsonSerializer.serialize(Object, Type, JsonSerializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom serializer again.
and
Invokes default deserialization on the specified object. It should never be invoked on the element received as a parameter of the JsonDeserializer.deserialize(JsonElement, Type, JsonDeserializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom deserializer again.
This concludes that below implementation will cause infinite loop and cause StackOverflowError eventually.
#Override
public JsonElement serialize(Animal src, Type typeOfSrc,
JsonSerializationContext context) {
return context.serialize(src);
}
I had the same problem, except my interface was of primitive type (CharSequence) and not JsonObject:
if (elem instanceof JsonPrimitive){
JsonPrimitive primitiveObject = (JsonPrimitive) elem;
Type primitiveType =
primitiveObject.isBoolean() ?Boolean.class :
primitiveObject.isNumber() ? Number.class :
primitiveObject.isString() ? String.class :
String.class;
return context.deserialize(primitiveObject, primitiveType);
}
if (elem instanceof JsonObject){
JsonObject wrapper = (JsonObject) elem;
final JsonElement typeName = get(wrapper, "type");
final JsonElement data = get(wrapper, "data");
final Type actualType = typeForName(typeName);
return context.deserialize(data, actualType);
}