2016-05-14 41 views
9

Sto utilizzando Retrofit 2 e Gson e ho problemi a deserializzare le risposte dalla mia API. Ecco il mio piano d'azione:Utilizzo di Gson e Retrofit 2 per deserializzare risposte API complesse

Ho un oggetto del modello di nome Employee che ha tre campi: id, name, age.

Ho un'API che restituisce un Employee oggetto singolare come questo:

{ 
    "status": "success", 
    "code": 200, 
    "data": { 
     "id": "123", 
     "id_to_name": { 
      "123" : "John Doe" 
     }, 
     "id_to_age": { 
      "123" : 30 
     } 
    } 
} 

e un elenco di Employee oggetti come questo:

{ 
    "status": "success", 
    "code": 200, 
    "data": [ 
     { 
      "id": "123", 
      "id_to_name": { 
       "123" : "John Doe" 
      }, 
      "id_to_age": { 
       "123" : 30 
      } 
     }, 
     { 
      "id": "456", 
      "id_to_name": { 
       "456" : "Jane Smith" 
      }, 
      "id_to_age": { 
       "456" : 35 
      } 
     }, 
    ] 
} 

ci sono tre cose principali da considerare qui:

  1. Le risposte API restituiscono in un wrapper generico, con la parte importante all'interno di e Campo data.
  2. I rendimenti API oggetti in un formato che non corrisponde direttamente ai campi del modello (ad esempio, il valore assunto da id_to_age esigenze mappare al campo age del modello)
  3. Il campo data nel La risposta API può essere un oggetto singolare o un elenco di oggetti.

Come implementare la deserializzazione con Gson in modo da gestire elegantemente queste tre custodie?

Idealmente, preferirei farlo interamente con TypeAdapter o TypeAdapterFactory invece di pagare la penalità delle prestazioni di JsonDeserializer. In ultima analisi, voglio finire con un'istanza di Employee o List<Employee> tale che soddisfa questa interfaccia:

public interface EmployeeService { 

    @GET("/v1/employees/{employee_id}") 
    Observable<Employee> getEmployee(@Path("employee_id") String employeeId); 

    @GET("/v1/employees") 
    Observable<List<Employee>> getEmployees(); 

} 

Questa domanda precedente ho postato discute mio primo tentativo di questo, ma non riesce a prendere in considerazione alcuni dei trucchi menzionato sopra: Using Retrofit and RxJava, how do I deserialize JSON when it doesn't map directly to a model object?

+0

si dice "il mio API". Se si ha accesso al back-end, è necessario rendere la serializzazione dell'età e un nome migliore sul lato server. – iagreen

+0

Non ho accesso. Con "mia API" mi riferisco all'API con cui lavoro. – user2393462435

+0

Perché non create Plain Old Java Objects che rappresentano le vostre risposte JSON e quindi le associano alla classe Employee? –

risposta

7

EDIT: update rilevanti: la creazione di una fabbrica di convertitore personalizzato funziona - la chiave per evitare un ciclo infinito attraverso ApiResponseConverterFactory 's è quello di chiamare di nextResponseBodyConverter retrofit, che permette di specificare una fabbrica per saltare. La chiave è che sarebbe Converter.Factory registrarsi con Retrofit, non uno TypeAdapterFactory per Gson. Ciò sarebbe in realtà preferibile poiché impedisce la doppia deserializzazione di ResponseBody (non è necessario deserializzare il corpo, quindi riconfezionarlo nuovamente come un'altra risposta).

See the gist here for an implementation example.

RISPOSTA ORIGINALE:

L'approccio ApiResponseAdapterFactory non funziona a meno che non si è disposti a avvolgere tutte le interfacce di servizio con ApiResponse<T>. Tuttavia, c'è un'altra opzione: intercettori OkHttp.

Ecco la nostra strategia:

  • Per la particolare configurazione retrofit, si registra un intercettore applicazione che intercetta il Response
  • Response#body() saranno deserializzato come ApiResponse e restituire una nuova Response dove il ResponseBody è solo il contenuto che vogliamo.

Così ApiResponse assomiglia:

public class ApiResponse { 
    String status; 
    int code; 
    JsonObject data; 
} 

ApiResponseInterceptor:

public class ApiResponseInterceptor implements Interceptor { 
    public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); 
    public static final Gson GSON = new Gson(); 

    @Override 
    public Response intercept(Chain chain) throws IOException { 
    Request request = chain.request(); 
    Response response = chain.proceed(request); 
    final ResponseBody body = response.body(); 
    ApiResponse apiResponse = GSON.fromJson(body.string(), ApiResponse.class); 
    body.close(); 

    // TODO any logic regarding ApiResponse#status or #code you need to do 

    final Response.Builder newResponse = response.newBuilder() 
     .body(ResponseBody.create(JSON, apiResponse.data.toString())); 
    return newResponse.build(); 
    } 
} 

Configurare l'OkHttp e Retrofit:

OkHttpClient client = new OkHttpClient.Builder() 
     .addInterceptor(new ApiResponseInterceptor()) 
     .build(); 
Retrofit retrofit = new Retrofit.Builder() 
     .client(client) 
     .build(); 

E Employee e EmployeeResponse dovrebbe seguire the adapter factory construct I wrote in the previous question. Ora tutti i campi ApiResponse devono essere consumati dall'intercettore e ogni chiamata di Retrofit effettuata deve solo restituire il contenuto JSON a cui sei interessato.

+0

Ottima idea! Ha assolutamente senso, e potrebbe anche essere un approccio utile anche per altre stranezze dell'API. Grazie ancora per il tuo aiuto in entrambe le domande. – user2393462435

+0

Nessun problema. Fammi sapere se ci sono problemi con questo approccio, questa volta dovrebbe essere buono! – ekchang

5

Suggerirei di usare uno JsonDeserializer perché non ci sono molti livelli di nidificazione nella risposta, quindi non sarà un grande successo di prestazioni.

Classi sarebbe simile a questa:

Interfaccia di servizio ha bisogno di essere regolato per la risposta generica:

interface EmployeeService { 

    @GET("/v1/employees/{employee_id}") 
    Observable<DataResponse<Employee>> getEmployee(@Path("employee_id") String employeeId); 

    @GET("/v1/employees") 
    Observable<DataResponse<List<Employee>>> getEmployees(); 

} 

Questa è una risposta di dati generici: modello

class DataResponse<T> { 

    @SerializedName("data") private T data; 

    public T getData() { 
     return data; 
    } 
} 

organigramma:

class Employee { 

    final String id; 
    final String name; 
    final int age; 

    Employee(String id, String name, int age) { 
     this.id = id; 
     this.name = name; 
     this.age = age; 
    } 

} 
deserializzatore

dipendenti:

class EmployeeDeserializer implements JsonDeserializer<Employee> { 

    @Override 
    public Employee deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) 
      throws JsonParseException { 

     JsonObject employeeObject = json.getAsJsonObject(); 
     String id = employeeObject.get("id").getAsString(); 
     String name = employeeObject.getAsJsonObject("id_to_name").entrySet().iterator().next().getValue().getAsString(); 
     int age = employeeObject.getAsJsonObject("id_to_age").entrySet().iterator().next().getValue().getAsInt(); 

     return new Employee(id, name, age); 
    } 
} 

Il problema con la risposta è che name e age sono contenuti all'interno di un oggetto JSON tuute le esigenze si traduce in una mappa in Java in modo che richiede un po 'più di lavoro per analizzarlo.

3

Basta creare il seguente TypeAdapterFactory.

public class ItemTypeAdapterFactory implements TypeAdapterFactory { 

    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { 

    final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type); 
    final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class); 

    return new TypeAdapter<T>() { 

     public void write(JsonWriter out, T value) throws IOException { 
      delegate.write(out, value); 
     } 

     public T read(JsonReader in) throws IOException { 

      JsonElement jsonElement = elementAdapter.read(in); 
      if (jsonElement.isJsonObject()) { 
       JsonObject jsonObject = jsonElement.getAsJsonObject(); 
       if (jsonObject.has("data")) { 
        jsonElement = jsonObject.get("data"); 
       } 
      } 

      return delegate.fromJsonTree(jsonElement); 
     } 
    }.nullSafe(); 
} 

}

e aggiungerlo nella vostra builder GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory()); 

o

yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());