Get nested JSON object with GSON using retrofit

前端 未结 13 759
挽巷
挽巷 2020-11-22 17:04

I\'m consuming an API from my android app, and all the JSON responses are like this:

{
    \'status\': \'OK\',
    \'reason\': \'Everything was fine\',
    \         


        
相关标签:
13条回答
  • 2020-11-22 17:21

    You would write a custom deserializer that returns the embedded object.

    Let's say your JSON is:

    {
        "status":"OK",
        "reason":"some reason",
        "content" : 
        {
            "foo": 123,
            "bar": "some value"
        }
    }
    

    You'd then have a Content POJO:

    class Content
    {
        public int foo;
        public String bar;
    }
    

    Then you write a deserializer:

    class MyDeserializer implements JsonDeserializer<Content>
    {
        @Override
        public Content deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException
        {
            // Get the "content" element from the parsed JSON
            JsonElement content = je.getAsJsonObject().get("content");
    
            // Deserialize it. You use a new instance of Gson to avoid infinite recursion
            // to this deserializer
            return new Gson().fromJson(content, Content.class);
    
        }
    }
    

    Now if you construct a Gson with GsonBuilder and register the deserializer:

    Gson gson = 
        new GsonBuilder()
            .registerTypeAdapter(Content.class, new MyDeserializer())
            .create();
    

    You can deserialize your JSON straight to your Content:

    Content c = gson.fromJson(myJson, Content.class);
    

    Edit to add from comments:

    If you have different types of messages but they all have the "content" field, you can make the Deserializer generic by doing:

    class MyDeserializer<T> implements JsonDeserializer<T>
    {
        @Override
        public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException
        {
            // Get the "content" element from the parsed JSON
            JsonElement content = je.getAsJsonObject().get("content");
    
            // Deserialize it. You use a new instance of Gson to avoid infinite recursion
            // to this deserializer
            return new Gson().fromJson(content, type);
    
        }
    }
    

    You just have to register an instance for each of your types:

    Gson gson = 
        new GsonBuilder()
            .registerTypeAdapter(Content.class, new MyDeserializer<Content>())
            .registerTypeAdapter(DiffContent.class, new MyDeserializer<DiffContent>())
            .create();
    

    When you call .fromJson() the type is carried into the deserializer, so it should then work for all your types.

    And finally when creating a Retrofit instance:

    Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(url)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build();
    
    0 讨论(0)
  • 2020-11-22 17:22

    Here's a Kotlin version based on the answers by Brian Roach and AYarulin.

    class RestDeserializer<T>(targetClass: Class<T>, key: String?) : JsonDeserializer<T> {
        val targetClass = targetClass
        val key = key
    
        override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): T {
            val data = json!!.asJsonObject.get(key ?: "")
    
            return Gson().fromJson(data, targetClass)
        }
    }
    
    0 讨论(0)
  • 2020-11-22 17:24

    As per answer of @Brian Roach and @rafakob i done this in the following way

    Json response from server

    {
      "status": true,
      "code": 200,
      "message": "Success",
      "data": {
        "fullname": "Rohan",
        "role": 1
      }
    }
    

    Common data handler class

    public class ApiResponse<T> {
        @SerializedName("status")
        public boolean status;
    
        @SerializedName("code")
        public int code;
    
        @SerializedName("message")
        public String reason;
    
        @SerializedName("data")
        public T content;
    }
    

    Custom serializer

    static class MyDeserializer<T> implements JsonDeserializer<T>
    {
         @Override
          public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
                        throws JsonParseException
          {
              JsonElement content = je.getAsJsonObject();
    
              // Deserialize it. You use a new instance of Gson to avoid infinite recursion
              // to this deserializer
              return new Gson().fromJson(content, type);
    
          }
    }
    

    Gson object

    Gson gson = new GsonBuilder()
                        .registerTypeAdapter(ApiResponse.class, new MyDeserializer<ApiResponse>())
                        .create();
    

    Api call

     @FormUrlEncoded
     @POST("/loginUser")
     Observable<ApiResponse<Profile>> signIn(@Field("email") String username, @Field("password") String password);
    
    restService.signIn(username, password)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribeOn(Schedulers.io())
                    .subscribe(new Observer<ApiResponse<Profile>>() {
                        @Override
                        public void onCompleted() {
                            Log.i("login", "On complete");
                        }
    
                        @Override
                        public void onError(Throwable e) {
                            Log.i("login", e.toString());
                        }
    
                        @Override
                        public void onNext(ApiResponse<Profile> response) {
                             Profile profile= response.content;
                             Log.i("login", profile.getFullname());
                        }
                    });
    
    0 讨论(0)
  • 2020-11-22 17:25

    Had the same problem couple of days ago. I've solve this using response wrapper class and RxJava transformer, which I think is quite flexiable solution:

    Wrapper:

    public class ApiResponse<T> {
        public String status;
        public String reason;
        public T content;
    }
    

    Custom exception to throw, when status is not OK:

    public class ApiException extends RuntimeException {
        private final String reason;
    
        public ApiException(String reason) {
            this.reason = reason;
        }
    
        public String getReason() {
            return apiError;
        }
    }
    

    Rx transformer:

    protected <T> Observable.Transformer<ApiResponse<T>, T> applySchedulersAndExtractData() {
        return observable -> observable
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .map(tApiResponse -> {
                    if (!tApiResponse.status.equals("OK"))
                        throw new ApiException(tApiResponse.reason);
                    else
                        return tApiResponse.content;
                });
    }
    

    Example usage:

    // Call definition:
    @GET("/api/getMyPojo")
    Observable<ApiResponse<MyPojo>> getConfig();
    
    // Call invoke:
    webservice.getMyPojo()
            .compose(applySchedulersAndExtractData())
            .subscribe(this::handleSuccess, this::handleError);
    
    
    private void handleSuccess(MyPojo mypojo) {
        // handle success
    }
    
    private void handleError(Throwable t) {
        getView().showSnackbar( ((ApiException) throwable).getReason() );
    }
    

    My topic: Retrofit 2 RxJava - Gson - "Global" deserialization, change response type

    0 讨论(0)
  • 2020-11-22 17:27

    This is the same solution as @AYarulin but assume the class name is the JSON key name. This way you only need to pass the Class name.

     class RestDeserializer<T> implements JsonDeserializer<T> {
    
        private Class<T> mClass;
        private String mKey;
    
        public RestDeserializer(Class<T> targetClass) {
            mClass = targetClass;
            mKey = mClass.getSimpleName();
        }
    
        @Override
        public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
                throws JsonParseException {
            JsonElement content = je.getAsJsonObject().get(mKey);
            return new Gson().fromJson(content, mClass);
    
        }
    }
    

    Then to parse sample payload from above, we can register GSON deserializer. This is problematic as the Key is case sensitive, so the case of the class name must match the case of the JSON key.

    Gson gson = new GsonBuilder()
    .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
    .build();
    
    0 讨论(0)
  • 2020-11-22 17:28

    Bit late but hopefully this will help someone.

    Just create following 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("content")) {
                            jsonElement = jsonObject.get("content");
                        }
                    }
    
                    return delegate.fromJsonTree(jsonElement);
                }
            }.nullSafe();
        }
    }
    

    and add it into your GSON builder :

    .registerTypeAdapterFactory(new ItemTypeAdapterFactory());
    

    or

     yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
    
    0 讨论(0)
提交回复
热议问题