Je consomme une API de mon application Android et toutes les réponses JSON ressemblent à ceci:
{
'status': 'OK',
'reason': 'Everything was fine',
'content': {
< some data here >
}
Le problème est que tous mes POJO ont des champs status
, reason
et que le champ content
contient le véritable POJO que je veux.
Existe-t-il un moyen de créer un convertisseur personnalisé de Gson pour extraire toujours le champ content
, de sorte que retrofit renvoie le POJO approprié?
Vous écririez un désérialiseur personnalisé qui renverrait l'objet incorporé.
Disons que votre JSON est:
{
"status":"OK",
"reason":"some reason",
"content" :
{
"foo": 123,
"bar": "some value"
}
}
Vous auriez alors un Content
POJO:
class Content
{
public int foo;
public String bar;
}
Ensuite, vous écrivez un désérialiseur:
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);
}
}
Maintenant, si vous construisez une Gson
avec GsonBuilder
et enregistrez le désérialiseur:
Gson gson =
new GsonBuilder()
.registerTypeAdapter(Content.class, new MyDeserializer())
.create();
Vous pouvez désérialiser votre JSON directement dans votre Content
:
Content c = gson.fromJson(myJson, Content.class);
Modifier pour ajouter des commentaires:
Si vous avez différents types de messages mais qu'ils ont tous le champ "contenu", vous pouvez rendre générique le désérialiseur en procédant comme suit:
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);
}
}
Vous devez simplement enregistrer une instance pour chacun de vos types:
Gson gson =
new GsonBuilder()
.registerTypeAdapter(Content.class, new MyDeserializer<Content>())
.registerTypeAdapter(DiffContent.class, new MyDeserializer<DiffContent>())
.create();
Lorsque vous appelez .fromJson()
, le type est transféré dans le désérialiseur, il devrait donc fonctionner pour tous vos types.
Et enfin, lors de la création d'une instance Retrofit:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
La solution @ BrianRoach est la bonne solution. Il convient de noter que dans le cas particulier où vous avez des objets personnalisés imbriqués nécessitant tous les deux une variable TypeAdapter
, vous devez enregistrer la variable TypeAdapter
avec l'instance new de GSON, sinon la seconde TypeAdapter
ne sera jamais appelée. En effet, nous créons une nouvelle instance Gson
dans notre désérialiseur personnalisé.
Par exemple, si vous aviez le json suivant:
{
"status": "OK",
"reason": "some reason",
"content": {
"foo": 123,
"bar": "some value",
"subcontent": {
"useless": "field",
"data": {
"baz": "values"
}
}
}
}
Et vous vouliez que ce JSON soit associé aux objets suivants:
class MainContent
{
public int foo;
public String bar;
public SubContent subcontent;
}
class SubContent
{
public String baz;
}
Vous devez enregistrer la variable SubContent
's TypeAdapter
. Pour être plus robuste, vous pouvez procéder comme suit:
public class MyDeserializer<T> implements JsonDeserializer<T> {
private final Class mNestedClazz;
private final Object mNestedDeserializer;
public MyDeserializer(Class nestedClazz, Object nestedDeserializer) {
mNestedClazz = nestedClazz;
mNestedDeserializer = nestedDeserializer;
}
@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
GsonBuilder builder = new GsonBuilder();
if (mNestedClazz != null && mNestedDeserializer != null) {
builder.registerTypeAdapter(mNestedClazz, mNestedDeserializer);
}
return builder.create().fromJson(content, type);
}
}
puis créez-le comme ceci:
MyDeserializer<Content> myDeserializer = new MyDeserializer<Content>(SubContent.class,
new SubContentDeserializer());
Gson gson = new GsonBuilder().registerTypeAdapter(Content.class, myDeserializer).create();
Cela pourrait facilement être utilisé pour le cas "contenu" imbriqué également en transmettant simplement une nouvelle instance de MyDeserializer
avec des valeurs null.
Un peu tard, mais j'espère que cela aidera quelqu'un.
Il suffit de créer ce qui suit 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();
}
}
et l'ajouter à votre constructeur GSON:
.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
ou
yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
En continuant l’idée de Brian, comme nous avons presque toujours de nombreuses ressources REST ayant chacune sa propre racine, il pourrait être utile de généraliser la désérialisation:
class RestDeserializer<T> implements JsonDeserializer<T> {
private Class<T> mClass;
private String mKey;
public RestDeserializer(Class<T> targetClass, String key) {
mClass = targetClass;
mKey = key;
}
@Override
public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
throws JsonParseException {
JsonElement content = je.getAsJsonObject().get(mKey);
return new Gson().fromJson(content, mClass);
}
}
Ensuite, pour analyser un exemple de charge utile ci-dessus, nous pouvons enregistrer le désérialiseur GSON:
Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
.build();
Avait le même problème il y a quelques jours. J'ai résolu ce problème en utilisant la classe de wrapper de réponse et le transformateur RxJava, ce qui me semble être une solution assez flexible:
Wrapper:
public class ApiResponse<T> {
public String status;
public String reason;
public T content;
}
Exception personnalisée à émettre, lorsque l'état n'est pas OK:
public class ApiException extends RuntimeException {
private final String reason;
public ApiException(String reason) {
this.reason = reason;
}
public String getReason() {
return apiError;
}
}
Transformateur Rx:
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;
});
}
Exemple d'utilisation:
// 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() );
}
Mon sujet: Retrofit 2 RxJava - Gson - Désérialisation "globale", changement du type de réponse
D'après les réponses de @Brian Roach et @rafakob, j'ai procédé de la manière suivante
Réponse Json du serveur
{
"status": true,
"code": 200,
"message": "Success",
"data": {
"fullname": "Rohan",
"role": 1
}
}
Classe de gestionnaire de données commune
public class ApiResponse<T> {
@SerializedName("status")
public boolean status;
@SerializedName("code")
public int code;
@SerializedName("message")
public String reason;
@SerializedName("data")
public T content;
}
Sérialiseur personnalisé
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);
}
}
Objet Gson
Gson gson = new GsonBuilder()
.registerTypeAdapter(ApiResponse.class, new MyDeserializer<ApiResponse>())
.create();
Appel api
@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());
}
});
Une meilleure solution pourrait être ceci ..
public class ApiResponse<T> {
public T data;
public String status;
public String reason;
}
Ensuite, définissez votre service comme ceci.
Observable<ApiResponse<YourClass>> updateDevice(..);
Voici une version de Kotlin basée sur les réponses de Brian Roach et 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)
}
}
C'est la même solution que @AYarulin mais supposons que le nom de la classe soit le nom de la clé JSON. De cette façon, il vous suffit de transmettre le nom de la classe.
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);
}
}
Ensuite, pour analyser l'échantillon de charge utile ci-dessus, nous pouvons enregistrer le désérialiseur GSON. Cela pose un problème car la clé est sensible à la casse. La casse du nom de la classe doit donc correspondre à la casse de la clé JSON.
Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();
Dans mon cas, la clé "contenu" changerait pour chaque réponse. Exemple:
// Root is hotel
{
status : "ok",
statusCode : 200,
hotels : [{
name : "Taj Palace",
location : {
lat : 12
lng : 77
}
}, {
name : "Plaza",
location : {
lat : 12
lng : 77
}
}]
}
//Root is city
{
status : "ok",
statusCode : 200,
city : {
name : "Vegas",
location : {
lat : 12
lng : 77
}
}
Dans de tels cas, j'ai utilisé une solution similaire à celle indiquée ci-dessus, mais je devais la modifier. Vous pouvez voir le Gist ici . C'est un peu trop gros pour le poster ici sur SOF.
L'annotation @InnerKey("content")
est utilisée et le reste du code est destiné à faciliter son utilisation avec Gson.
Une autre solution simple:
JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
N'oubliez pas les annotations @SerializedName
et @Expose
pour tous les membres du groupe et les membres du groupe Inner les plus désérialisés à partir de JSON par GSON.