AndroidアプリからAPIを使用していますが、JSON応答はすべて次のようになります。
{
'status': 'OK',
'reason': 'Everything was fine',
'content': {
< some data here >
}
問題は、すべてのPOJOにstatus
、reason
フィールドがあり、content
フィールド内に実際に必要なPOJOがあることです。
常にcontent
フィールドを抽出するためにGsonのカスタムコンバーターを作成する方法はありますか。したがって、retrofitは適切なPOJOを返しますか?
埋め込みオブジェクトを返すカスタムデシリアライザーを作成します。
JSONが次のようになっているとしましょう:
{
"status":"OK",
"reason":"some reason",
"content" :
{
"foo": 123,
"bar": "some value"
}
}
その後、Content
POJOがあります。
class Content
{
public int foo;
public String bar;
}
次に、デシリアライザーを作成します。
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);
}
}
ここで、Gson
を使用してGsonBuilder
を構築し、デシリアライザーを登録する場合:
Gson gson =
new GsonBuilder()
.registerTypeAdapter(Content.class, new MyDeserializer())
.create();
JSONを直接Content
にデシリアライズできます:
Content c = gson.fromJson(myJson, Content.class);
コメントから追加するために編集:
さまざまなタイプのメッセージがあり、それらにすべて「コンテンツ」フィールドがある場合、次の操作を行うことにより、デシリアライザーをジェネリックにすることができます。
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);
}
}
タイプごとにインスタンスを登録する必要があります。
Gson gson =
new GsonBuilder()
.registerTypeAdapter(Content.class, new MyDeserializer<Content>())
.registerTypeAdapter(DiffContent.class, new MyDeserializer<DiffContent>())
.create();
.fromJson()
を呼び出すと、型がデシリアライザーに渡されるため、すべての型で機能するはずです。
最後に、Retrofitインスタンスを作成するとき:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
@BrianRoachのソリューションは正しいソリューションです。両方がカスタムTypeAdapter
を必要とするカスタムオブジェクトをネストしている特殊なケースでは、GSONの新しいインスタンスでTypeAdapter
を登録する必要があります。そうしないと、2番目のTypeAdapter
が呼び出されません。これは、カスタムデシリアライザー内に新しいGson
インスタンスを作成しているためです。
たとえば、次のjsonがある場合:
{
"status": "OK",
"reason": "some reason",
"content": {
"foo": 123,
"bar": "some value",
"subcontent": {
"useless": "field",
"data": {
"baz": "values"
}
}
}
}
また、このJSONを次のオブジェクトにマッピングする必要がありました。
class MainContent
{
public int foo;
public String bar;
public SubContent subcontent;
}
class SubContent
{
public String baz;
}
SubContent
のTypeAdapter
を登録する必要があります。より堅牢にするために、次のことを実行できます。
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);
}
}
そして次のように作成します:
MyDeserializer<Content> myDeserializer = new MyDeserializer<Content>(SubContent.class,
new SubContentDeserializer());
Gson gson = new GsonBuilder().registerTypeAdapter(Content.class, myDeserializer).create();
これは、null値を持つMyDeserializer
の新しいインスタンスを渡すだけで、ネストされた「コンテンツ」の場合にも簡単に使用できます。
少し遅れますが、うまくいけば誰かの助けになるでしょう。
次の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();
}
}
gSONビルダーに追加します。
.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
または
yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
ブライアンのアイデアを続けると、ほとんど常にRESTリソースがあり、それぞれに独自のルートがあるため、逆シリアル化を一般化すると便利です。
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);
}
}
次に、上記のサンプルペイロードを解析するために、GSONデシリアライザーを登録できます。
Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
.build();
数日前に同じ問題を抱えていました。応答ラッパークラスとRxJavaトランスフォーマーを使用してこれを解決しましたが、これは非常に柔軟なソリューションだと思います。
ラッパー:
public class ApiResponse<T> {
public String status;
public String reason;
public T content;
}
ステータスがOKでない場合にスローするカスタム例外:
public class ApiException extends RuntimeException {
private final String reason;
public ApiException(String reason) {
this.reason = reason;
}
public String getReason() {
return apiError;
}
}
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;
});
}
使用例:
// 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() );
}
私のトピック: Retrofit 2 RxJava-Gson-"Global" deserialization、change response type
@Brian Roachと@rafakobの回答に従って、私はこれを次のように行いました
サーバーからのJSON応答
{
"status": true,
"code": 200,
"message": "Success",
"data": {
"fullname": "Rohan",
"role": 1
}
}
共通データハンドラクラス
public class ApiResponse<T> {
@SerializedName("status")
public boolean status;
@SerializedName("code")
public int code;
@SerializedName("message")
public String reason;
@SerializedName("data")
public T content;
}
カスタムシリアライザー
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オブジェクト
Gson gson = new GsonBuilder()
.registerTypeAdapter(ApiResponse.class, new MyDeserializer<ApiResponse>())
.create();
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());
}
});
より良い解決策はこれかもしれません。
public class ApiResponse<T> {
public T data;
public String status;
public String reason;
}
次に、このようにサービスを定義します。
Observable<ApiResponse<YourClass>> updateDevice(..);
Brian RoachとAYarulinの回答に基づいたKotlinバージョンです。
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)
}
}
これは@AYarulinと同じソリューションですが、クラス名がJSONキー名であると想定しています。この方法では、クラス名を渡すだけで済みます。
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);
}
}
次に、上記のサンプルペイロードを解析するために、GSONデシリアライザーを登録できます。キーでは大文字と小文字が区別されるため、これは問題です。そのため、クラス名の大文字と小文字はJSONキーの大文字と小文字を一致させる必要があります。
Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();
私の場合、「コンテンツ」キーは応答ごとに変わります。例:
// 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
}
}
そのような場合、私は上記と同様のソリューションを使用しましたが、それを微調整する必要がありました。要点を見ることができます こちら 。 SOFに投稿するには少々大きすぎます。
注釈@InnerKey("content")
が使用され、残りのコードはGsonでの使用を容易にするためのものです。
別の簡単なソリューション:
JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
GSONによってJSONから最も逆シリアル化されたすべてのクラスメンバーおよび内部クラスメンバーの@SerializedName
および@Expose
注釈を忘れないでください。