2

The API I'm working with returns objects (and their containing objects) in a "flat" format and I'm having trouble getting this to work elegantly with Retrofit and RxJava.

Consider this JSON response for an /employees/{id} endpoint:

{
    "id": "123",
    "id_to_name": {
        "123" : "John Doe"
    },
    "id_to_age": {
        "123" : 30
    }
}

Using Retrofit and RxJava, how do I deserialize this to a Employee object with fields for name and age?

Ideally I'd like RxJava's onNext method to be called with an Employee object. Is this possible? Could this perhaps be done with some type of custom deserializer subclass (I'm using Gson at the moment)?

I realize I could create an EmployeeResponse object that maps directly to the JSON response, but having to map the EmployeeResponse to the Employee object every time I use this in an activity seems kind of unfortunate. It also gets much more complicated when the flat response also contains other objects that need to get deserialized and set as fields on the Employee.

Is there a better way?

user2393462435
  • 2,652
  • 5
  • 37
  • 45
  • you can have retrofit return a `Observable` and `map((jsonObject) -> gson.fromJson(jsonObject.getJsonElement("data"), Employee.class))` – njzk2 May 12 '16 at 02:42

1 Answers1

3

The complete solution to this will seem like a lot, but this will let you write Retrofit interfaces with Employee instead of EmployeeResponse. Here's the game plan:

  • You will still need both EmployeeResponse and Employee objects, where EmployeeResponse just maps exactly to what you'd get from the API. Treat the response as a builder for Employee and write a static factory method that returns an Employee from an EmployeeResponse, ie. Employee employee = Employee.newInstance(response);
  • You will be creating a custom TypeAdapterFactory for Gson. When Gson sees you request a Employee object, we will have the TypeAdapter actually create an EmployeeResponse, then return the Employee via the static factory method described above.

Your TypeAdapterFactory will look something like this:

public class EmployeeAdapterFactory implements TypeAdapterFactory {
  @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    return type.getRawType() == Employee.class
           ? (TypeAdapter<T>) employeeAdapter(gson, (TypeToken<Employee>) type)
           : null;
  }

  private TypeAdapter<Employee> employeeAdapter(Gson gson, TypeToken<Employee> type) {
    return new TypeAdapter<Employee>() {
      @Override public void write(JsonWriter out, Employee value) throws IOException {
        // TODO serialization logic to go from an Employee back to EmployeeResponse structure, if necessary
      }

      @Override public Employee read(JsonReader in) throws IOException {
        return Employee.newInstance(gson.fromJson(in, EmployeeResponse.class));
      }
    };
  }
}

Register the factory when you make Gson:

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(new EmployeeAdapterFactory())
    .build();

Retrofit retrofit = new Retrofit.Builder().baseUrl("https://foo.bar")
    .addConverterFactory(GsonConverterFactory.create(gson))
    ... // your other Retrofit configs, like RxJava call adapter factory
    .build();

And now you can safely define all your Retrofit interfaces with Employee instead of EmployeeResponse.

ekchang
  • 939
  • 5
  • 11
  • I had a feeling it was something along these lines. Thank you very much! – user2393462435 May 08 '16 at 23:39
  • one thing I forgot is that my response actually comes back in a generic wrapper format similar to this: http://stackoverflow.com/questions/23070298/get-nested-json-object-with-gson-using-retrofit ... I can make this work by having my service interface return Observable> but it there a way I can adapt this to still return Observable instead? – user2393462435 May 11 '16 at 03:14
  • If every response follows that structure, then you can register an `ApiResponseAdapterFactory` which always creates a `TypeAdapter` (no checks unlike the answer above). Then in `read` you can simply deserialize an `ApiResponse response = gson.fromJson(in, type.getRawtype())` and return `response.body()`. Sorry, this is a little ugly to respond in a comment, if you want to edit your original question I can edit my response. – ekchang May 12 '16 at 01:27
  • Edited. Thank you! – user2393462435 May 12 '16 at 02:08
  • I forgot a few other "gotchas," so I ended up posting a new question: http://stackoverflow.com/questions/37231894/using-gson-and-retrofit-2-to-deserialize-complex-api-responses – user2393462435 May 14 '16 at 21:10