1

I'm using Retrofit in my Android app that utilizes external API. The problem is in the response that I can't figure out how to deserialize to return list of objects. JSON I get has such format:

{
    "attribute_1": "value",
    "attribute_2": "value",
    "member_1": {
        "param1": "value1",
        "param2": "value2"
    },
    "member_2": {
        "param1": "value1",
        "param2": "value2"
    },
    ...
}

API call in Retrofit looks like this:

@GET("apiednpoint/path")
Call<List<Member>> getMembers();

I want to ignore attributes and retrieve List<Member> from this response. I know I can create a custom deserializer to ignore some fields within JSON like here and convert members into an array like here, but in the second link I'd need a wrapper class form my List<Member> that I expect. Is it possible to do without wrapper around my list / array that expect?

Community
  • 1
  • 1
Luke
  • 2,539
  • 2
  • 23
  • 40

2 Answers2

1

It's not free but it's possible with Retrofit (and not easy with standalone Gson because it would require more "magic"). The following solution is far from perfect, but you can improve it according to your needs. Let's say you have he following Member mapping:

final class Member {

    final String param1 = null;
    final String param2 = null;

}

And the following service:

interface IService {

    @GET("/")
    @ByRegExp("member_.+")
    Call<List<Member>> getMembers();

}

Note that @ByRegExp is a custom annotation that will be processed below. The annotation declaration:

@Retention(RUNTIME)
@Target(METHOD)
@interface ByRegExp {

    String value();

}

Now, the following code borrows some code from my debug/mocking code, but it can be easily translated to your real code:

// This is just a mocked HTTP client that always returns your members.json
final OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(staticResponse(Q43925012.class, "members.json"))
        .build();
// Gson stuff
final Gson gson = new GsonBuilder()
        // ... configure your Gson here ...
        .create();
final Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://whatever")
        .client(client)
        .addConverterFactory(new Converter.Factory() {
            @Override
            public Converter<ResponseBody, ?> responseBodyConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
                // Checking if the method is declared with @ByRegExp annotation
                final ByRegExp byRegExp = findByRegExp(annotations);
                if ( byRegExp != null ) {
                    // If so, then compile the regexp pattern
                    final Pattern pattern = Pattern.compile(byRegExp.value());
                    // And resolve the list element type
                    final Type listElementType = getTypeParameter0(type);
                    // Obtaining the original your-type list type adapter
                    final TypeAdapter<?> listElementTypeAdapter = gson.getAdapter(TypeToken.get(listElementType));
                    return (Converter<ResponseBody, Object>) responseBody -> {
                        try {
                            // Getting input stream from the response body and converting it to a JsonReader -- a low level JSON parser
                            final JsonReader jsonReader = new JsonReader(new InputStreamReader(responseBody.byteStream()));
                            final List<Object> list = new ArrayList<>();
                            // Make sure that the first token is `{`
                            jsonReader.beginObject();
                            // And iterate over each JSON property
                            while ( jsonReader.hasNext() ) {
                                final String name = jsonReader.nextName();
                                final Matcher matcher = pattern.matcher(name);
                                // Check if the property need matches the pattern
                                if ( matcher.matches() ) {
                                    // And if so, just deserialize it and put it to the result list
                                    final Object element = listElementTypeAdapter.read(jsonReader);
                                    list.add(element);
                                } else {
                                    // Or skip the value entirely
                                    jsonReader.skipValue();
                                }
                            }
                            // make sure that the current JSON token is `{` - NOT optional
                            jsonReader.endObject();
                            return list;
                        } finally {
                            responseBody.close();
                        }
                    };
                }
                return super.responseBodyConverter(type, annotations, retrofit);
            }

            private ByRegExp findByRegExp(final Annotation[] annotations) {
                for ( final Annotation annotation : annotations ) {
                    if ( annotation instanceof ByRegExp ) {
                        return (ByRegExp) annotation;
                    }
                }
                return null;
            }

            // Trying to resolve how List<E> is parameterized (or raw if not)
            private Type getTypeParameter0(final Type type) {
                if ( !(type instanceof ParameterizedType) ) {
                    return Object.class;
                }
                final ParameterizedType parameterizedType = (ParameterizedType) type;
                return parameterizedType.getActualTypeArguments()[0];
            }
        })
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build();
final IService service = retrofit.create(IService.class);
final List<Member> members = service.getMembers()
        .execute()
        .body();
for ( final Member member : members ) {
    System.out.println(member.param1 + ", " + member.param2);
}

Output:

value1, value2
value1, value2

I don't think it can be easier to implement (in terms of Gson/Retrofit interaction), but I hope it helps.

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

I Believe this small code will help you (Asumming that json is the JSON response that you described):

public List<Member> deserialize(String json) {
    com.google.gson.Gson gson;
    com.google.gson.JsonParser.JsonParser parser;
    com.google.gson.JsonElement elem;
    com.google.gson.JsonObject jsonObj;
    java.util.List<Member> members;

    gson    = new Gson();
    parser  = new JsonParser();
    elem    = parser.parse(json);
    jsonObj = element.getAsJsonObject();
    members = new ArrayList<>();

    for (Map.Entry<String, JsonElement> entry : jsonObj.entrySet()) {
        if (entry.getKey().startsWith("member_")) {
            members.add( gson.fromJson(entry.getValue(), Member.class) );
        }
    }
    return members;
}
Carlitos Way
  • 3,279
  • 20
  • 30
  • Thanks for the response, however it's not clear to me how can I register such deserializer with `new GsonBuilder().registerTypeAdapter(?.class, new DeserializerClass())` – Luke May 14 '17 at 14:17
  • No, you don´t have to register anything (first, because you JSON response have a dynamic number of `member` attributes and two, you cannot create a class that matches such configuration) ... you only have to invoke the given method of your `JSON` response!! – Carlitos Way May 15 '17 at 16:47