50

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?

three-cups
  • 4,375
  • 3
  • 31
  • 41
  • 1
    The question is, who generates Json like this? Is it valid Json? If it is, Gson should handle it. If not, the "real" solution should be to fix the producer. – Nilzor Jul 04 '14 at 07:02
  • 9
    I totally agree that this is not a great way to write JSON. Unfortunately, we don't always have control over the data that we consume, so fixing the producer is not always an option. It is valid JSON, since JSON is schema-less. – three-cups Jul 04 '14 at 14:00
  • Agreed with @three-cups – Fazal Hussain Jun 13 '19 at 07:31

4 Answers4

81

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

Nublodeveloper
  • 1,301
  • 13
  • 20
three-cups
  • 4,375
  • 3
  • 31
  • 41
  • Thanks, works perfectly. I had a case where the JSON would return a single object or set of them. – gnosio Dec 11 '11 at 01:58
  • Thanks. This is great. I needed two different TypeAdapters so I chained them by adding another .registerTypeAdapter – Nova Entropy Feb 27 '13 at 10:50
  • 6
    Why Gson is not putting just annotation to accept array or object of same format? This is highly demanded. – Bhavesh Hirpara Oct 13 '15 at 06:20
  • Cool ! and just remind for others, don't forget to `implements JsonDeserializer>`, otherwise it doesn't work – Weiyi Mar 30 '18 at 09:54
9

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();
halfer
  • 19,824
  • 17
  • 99
  • 186
levavare
  • 494
  • 1
  • 4
  • 13
2

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
}
Eric
  • 16,397
  • 8
  • 68
  • 76
0

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);
NameSpace
  • 10,009
  • 3
  • 39
  • 40