0

I have a JSON response from an API like this:

{"asalas77":
    {"id":23519033,"name":"Asalas77","profileIconId":22,"revisionDate":1487214366000,"summonerLevel":30}
}

And I need to extract the inner object from it. I tried using a deserializer like shown in this question Get nested JSON object with GSON using retrofit but it doesn't work for me.

public class SummonerDeserializer implements JsonDeserializer<Summoner> {
@Override
public Summoner deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
        throws JsonParseException {
    long id = je.getAsJsonObject().get("id").getAsLong();
    String name = je.getAsJsonObject().get("name").getAsString();
    int profileIconId = je.getAsJsonObject().get("profileIconId").getAsInt();
    long revisionDate = je.getAsJsonObject().get("revisionDate").getAsLong();
    long summonerLevel = je.getAsJsonObject().get("summonerLevel").getAsLong();
    Summoner s = new Summoner();
    s.setId(id);
    s.setName(name);
    s.setProfileIconId(profileIconId);
    s.setRevisionDate(revisionDate);
    s.setSummonerLevel(summonerLevel);

    return s;
}
}

But the problem is I can't access the inner fields from JsonElement je and the name asalas77 is a variable (it's a search query) so I can't extract the inner object directly.

Community
  • 1
  • 1
Asalas77
  • 612
  • 4
  • 15
  • 26

1 Answers1

1

You must have a wrapper class in order not to clash deserialization strategies. Assume it's as follows:

final class SummonerResponse {

    private final Summoner summoner;

    private SummonerResponse(final Summoner summoner) {
        this.summoner = summoner;
    }

    static SummonerResponse summonerResponse(final Summoner summoner) {
        return new SummonerResponse(summoner);
    }

    Summoner getSummoner() {
        return summoner;
    }

}

Then you can either create a custom response deserializer:

final class SummonerWrapperDeserializer
        implements JsonDeserializer<SummonerResponse> {

    private static final JsonDeserializer<SummonerResponse> summonerDeserializer = new SummonerWrapperDeserializer();

    private SummonerWrapperDeserializer() {
    }

    static JsonDeserializer<SummonerResponse> getSummonerResponseDeserializer() {
        return summonerDeserializer;
    }

    @Override
    public SummonerResponse deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        // Pick the root as a JSON object
        final JsonObject outerJsonObject = jsonElement.getAsJsonObject();
        // And check how many properties does it have
        final Iterable<? extends Entry<String, JsonElement>> outerJsonObjectEntries = outerJsonObject.entrySet();
        if ( outerJsonObject.size() != 1 ) {
            throw new JsonParseException("Expected one property object, the actual properties are: " + getPropertyName(outerJsonObjectEntries));
        }
        // If it has only one property, just get the property and take its inner value
        final JsonElement innerJsonElement = outerJsonObjectEntries.iterator().next().getValue();
        // Once it's obtained, just delegate the parsing to a downstream parser - no need to create Summoner instances by hands
        return summonerResponse(context.deserialize(innerJsonElement, Summoner.class));
    }

    private static Set<String> getPropertyName(final Iterable<? extends Entry<String, JsonElement>> entries) {
        final Set<String> keys = new LinkedHashSet<>();
        for ( final Entry<String, JsonElement> entry : entries ) {
            keys.add(entry.getKey());
        }
        return keys;
    }

}

Or save some memory (the JSON (de)serializers require some memory because they work with JSON trees) and create a more low level type adapter:

final class SummonerResponseTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory summonerResponseTypeAdapterFactory = new SummonerResponseTypeAdapterFactory();

    private SummonerResponseTypeAdapterFactory() {
    }

    static TypeAdapterFactory getSummonerResponseTypeAdapterFactory() {
        return summonerResponseTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        // Check if we can handle SummonerResponse. Classes can be compared with `==`
        if ( typeToken.getRawType() == SummonerResponse.class ) {
            final TypeAdapter<SummonerResponse> typeAdapter = getSummonerResponseTypeAdapter(gson);
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
            return castTypeAdapter;
        }
        return null;
    }

}
final class SummonerResponseTypeAdapter
        extends TypeAdapter<SummonerResponse> {

    private final Gson gson;

    private SummonerResponseTypeAdapter(final Gson gson) {
        this.gson = gson;
    }

    static TypeAdapter<SummonerResponse> getSummonerResponseTypeAdapter(final Gson gson) {
        return new SummonerResponseTypeAdapter(gson);
    }

    @Override
    @SuppressWarnings("resource")
    public void write(final JsonWriter out, final SummonerResponse summonerResponse)
            throws IOException {
        // The incoming object may be null
        if ( summonerResponse == null && gson.serializeNulls() ) {
            out.nullValue();
            return;
        }
        // Generate the inner object
        out.beginObject();
        out.name(summonerResponse.getSummoner().name);
        gson.toJson(summonerResponse.getSummoner(), Summoner.class, out);
        out.endObject();
    }

    @Override
    public SummonerResponse read(final JsonReader in)
            throws IOException {
        // is it a null?
        if ( in.peek() == NULL ) {
            return null;
        }
        // make sure that the inner read JSON contains an inner object
        in.beginObject();
        // ignore the name
        in.nextName();
        // delegate parsing to the backing Gson instance in order to apply downstream parsing
        final Summoner summoner = gson.fromJson(in, Summoner.class);
        // check if there are more properties within the inner object
        if ( in.peek() == NAME ) {
            throw new MalformedJsonException("Unexpected: " + in.nextName());
        }
        // consume the "}" token
        in.endObject();
        return summonerResponse(summoner);
    }

}

Then any of the options above can be used like this:

final Gson gson = new GsonBuilder()
        .registerTypeAdapter(SummonerResponse.class, getSummonerResponseDeserializer())
        .create();
final SummonerResponse summonerResponse = gson.fromJson(JSON, SummonerResponse.class);
final Summoner summoner = summonerResponse.getSummoner();
out.println(summoner.id + " => " + summoner.name);

or

final Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(getSummonerResponseTypeAdapterFactory())
        .create();
final SummonerResponse summonerResponse = gson.fromJson(JSON, SummonerResponse.class);
final Summoner summoner = summonerResponse.getSummoner();
out.println(summoner.id + " => " + summoner.name);
out.println(gson.toJson(summonerResponse));

The outputs are

23519033 => Asalas77

and

23519033 => Asalas77
{"Asalas77":{"id":23519033,"name":"Asalas77","profileIconId":22,"revisionDate":1487214366000,"summonerLevel":30}}

respectively.

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105