I\'m consuming an API from my android app, and all the JSON responses are like this:
{
\'status\': \'OK\',
\'reason\': \'Everything was fine\',
\
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();
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)
}
}
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());
}
});
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
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();
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());