web-dev-qa-db-ja.com

Gsonに間違ったタイプの例外をスローさせます

プロジェクト内でGsonを使用して、JSON文字列をJavaオブジェクトに逆シリアル化します。リクエストを行うと、サーバーからの明確な応答が期待されます。サーバーは、私が期待する明確に定義された応答を返すか、OR(これも定義された)エラーオブジェクトを返します。

明確にするために:次のような単純なオブジェクトがあるとします。

class Dummy{
   private String foo;
   private int bar;
}

および次のようなエラーオブジェクト:

class ErrorHolder{
   private RequestError error;
}

class RequestError{
    private String publicMsg;
    private String msg;
}

次のようなサーバー応答を受け取った場合

{"foo":"Hello World", "bar":3 }

すべてが期待どおりに機能します。

しかし、応答がこのような場合

{"error":{"publicMsg":"Something bad happened", msg:"you forgot requesting some parameter"}}

Dummyfooで、nullが0の、一種のbarオブジェクトを取得します。 Gsonのドキュメント(fromJson)には、次のことが明確に記載されています。

jsonSyntaxExceptionをスローします-jsonがclassOfT型のオブジェクトの有効な表現でない場合

したがって、次のように2番目の応答を解析しようとすると、JsonSyntaxExceptionが発生することが予想されました。

Dummy dummy = Gson.fromJson(secondResponse, Dummy.class);

jsonはダミーオブジェクトではなく、ErrorHolderオブジェクトを表すためです。

だから私の質問は:Gsonがどういうわけか間違ったタイプを検出し、私に例外をスローする方法はありますか?

15
Rafael T

残念ながら、ドキュメントは少し誤解を招く可能性があります。

クラスにJSONのタイプと一致しないフィールドがある場合にのみ例外がスローされ、それでもそれを修正するためにいくつかのクレイジーなことが行われます(JSONのintをに変換します)たとえば、クラスのString)。 POJOにDateフィールドのようなものがあり、JSONでintに遭遇した場合、それはスローされます。 JSONには存在するがPOJOには存在しないフィールドは黙って無視され、JSONには存在しないが、POJOには存在するフィールドはnullに設定されます。

現在、GSONは、POJOのフィールドに_@Required_アノテーションのようなものがあるような「厳密な」逆シリアル化のメカニズムを提供していません。

あなたの場合... POJOを拡張して内部エラーオブジェクトを含めるだけです...次のようなものです。

_class Dummy {
   private String foo;
   private int bar;
   private Error error;

   private class Error {
        String publicMsg;
        String msg;
   }

   public boolean isError() {
       return error != null;
   }

   // setters and getters for your data, the error msg, etc.
}
_

他のオプションは、JSONが次のようなエラーの場合に例外をスローするカスタムデシリアライザーを作成することです。

_class MyDeserializer implements JsonDeserializer<Dummy>
{
    @Override
    public Dummy deserialize(JsonElement json, Type typeOfT, 
                              JsonDeserializationContext context)
                    throws JsonParseException
    {
        JsonObject jsonObject = (JsonObject) json;

        if (jsonObject.get("error") != null)
        {
            throw new JsonParseException("Error!");
        }

        return new Gson().fromJson(json, Dummy.class);
    }
} 
_

編集して追加:誰かが最近これに賛成して、それを読み直しました。「ええと、あなたはこれを自分で行うことができ、それは便利かもしれない」と思いました。

これは、OPが望んでいたことを正確に実行する再利用可能なデシリアライザーと注釈です。制限は、POJOがカスタムデシリアライザーをそのまま必要とする場合、もう少し進んで、コンストラクターでGsonオブジェクトを渡してオブジェクト自体にデシリアライズするか、アノテーションをチェックアウトする必要があることです。別のメソッドに変換し、デシリアライザーで使用します。独自の例外を作成してJsonParseExceptionに渡すことで例外処理を改善し、呼び出し元のgetCause()を介して検出できるようにすることもできます。

とはいえ、ほとんどの場合、これは機能します。

_public class App
{

    public static void main(String[] args)
    {
        Gson gson =
            new GsonBuilder()
            .registerTypeAdapter(TestAnnotationBean.class, new AnnotatedDeserializer<TestAnnotationBean>())
            .create();

        String json = "{\"foo\":\"This is foo\",\"bar\":\"this is bar\"}";
        TestAnnotationBean tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);

        json = "{\"foo\":\"This is foo\"}";
        tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);

        json = "{\"bar\":\"This is bar\"}";
        tab = gson.fromJson(json, TestAnnotationBean.class);
        System.out.println(tab.foo);
        System.out.println(tab.bar);
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonRequired
{
}

class TestAnnotationBean
{
    @JsonRequired public String foo;
    public String bar;
}

class AnnotatedDeserializer<T> implements JsonDeserializer<T>
{

    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException
    {
        T pojo = new Gson().fromJson(je, type);

        Field[] fields = pojo.getClass().getDeclaredFields();
        for (Field f : fields)
        {
            if (f.getAnnotation(JsonRequired.class) != null)
            {
                try
                {
                    f.setAccessible(true);
                    if (f.get(pojo) == null)
                    {
                        throw new JsonParseException("Missing field in JSON: " + f.getName());
                    }
                }
                catch (IllegalArgumentException ex)
                {
                    Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
                }
                catch (IllegalAccessException ex)
                {
                    Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return pojo;

    }
}
_

出力:

これはfooです
これはbarです
これはfooです
 null 
スレッド "main"の例外com.google.gson.JsonParseException:Missing JSONのフィールド:foo 
25
Brian Roach

ネストされたオブジェクトを処理し、他にいくつかの小さな変更を加えたBrianのソリューションの更新バージョンを作成しました。このコードには、JsonRequiredで注釈が付けられたフィールドを持つクラスを認識するGsonオブジェクトを作成するためのより単純なビルダーも含まれています。

import Java.lang.annotation.ElementType;
import Java.lang.annotation.Retention;
import Java.lang.annotation.RetentionPolicy;
import Java.lang.annotation.Target;
import Java.lang.reflect.Field;
import Java.lang.reflect.Type;
import Java.util.List;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;

public class AnnotatedDeserializer<T> implements JsonDeserializer<T> {

private final Gson gson = new Gson();

public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {

    T target = gson.fromJson(je, type);
    checkRequired(target);
    return target;
}

private List<Field> findMissingFields(Object target, List<Field> invalidFields) {

    for (Field field : target.getClass().getDeclaredFields()) {
        if (field.getAnnotation(JsonRequired.class) != null) {

            Object fieldValue = ReflectionUtil.getFieldValue(target, field);

            if (fieldValue == null) {
                invalidFields.add(field);
                continue;
            }

            if (!isPrimitive(fieldValue)) {
                findMissingFields(fieldValue, invalidFields);
            }
        }
    }
    return invalidFields;
}

private void checkRequired(Object target) {

    List<Field> invalidFields = Lists.newArrayList();
    findMissingFields(target, invalidFields);

    if (!invalidFields.isEmpty()) {
        throw new JsonParseException("Missing JSON required fields: {"
                + FluentIterable.from(invalidFields).transform(toMessage).join(Joiner.on(", ")) + "}");
    }
}

static Function<Field, String> toMessage = new Function<Field, String>() {
    @Override
    public String apply(Field field) {
        return field.getDeclaringClass().getName() + "/" + field.getName();
    }
};

private boolean isPrimitive(Object target) {

    for (Class<?> primitiveClass : Primitives.allPrimitiveTypes()) {
        if (primitiveClass.equals(target.getClass())) {
            return true;
        }
    }
    return false;
}

public static class RequiredFieldAwareGsonBuilder {

    private GsonBuilder gsonBuilder;

    private RequiredFieldAwareGsonBuilder(GsonBuilder gsonBuilder) {
        this.gsonBuilder = gsonBuilder;
    }

    public static RequiredFieldAwareGsonBuilder builder() {
        return new RequiredFieldAwareGsonBuilder(new GsonBuilder());
    }

    public <T> RequiredFieldAwareGsonBuilder withRequiredFieldAwareType(Class<T> classOfT) {
        gsonBuilder.registerTypeAdapter(classOfT, new AnnotatedDeserializer<T>());
        return this;
    }

    public Gson build() {
        return gsonBuilder.create();
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public static @interface JsonRequired {
}
}

そしてReflectionユーティリティ

import Java.lang.reflect.Field;

public final class ReflectionUtil {

private ReflectionUtil() {
}

public static Object getFieldValue(Object target, Field field) {
    try {
        boolean originalFlag = changeAccessibleFlag(field);
        Object fieldValue = field.get(target);
        restoreAccessibleFlag(field, originalFlag);
        return fieldValue;
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Failed to access field " + field.getDeclaringClass().getName() + "/"
                + field.getName(), e);
    }
}

private static void restoreAccessibleFlag(Field field, boolean flag) {
    field.setAccessible(flag);
}

private static boolean changeAccessibleFlag(Field field) {
    boolean flag = field.isAccessible();
    field.setAccessible(true);
    return flag;
}
}

Guiceを使用する場合は、このようなものをモジュールに追加して、Gsonオブジェクトを挿入できます。

@Provides
@Singleton
static Gson provideGson() {
    return RequiredFieldAwareGsonBuilder.builder().withRequiredFieldAwareType(MyType1.class)
            .withRequiredFieldAwareType(MyType2.class).build();
}
3

私は選択したソリューションのファンではありません。それは機能しますが、Gsonを使用する方法ではありません。 Gsonは、特定のJSONスキーマをオブジェクトにマップします。その逆も同様です。理想的には、使用しているJSONは整形式です(したがって、JSON形式を制御できる場合は、変更することを検討してください)が、そうでない場合は、受け取ると予想されるすべてのケースを処理するように解析オブジェクトを設計する必要があります。

カスタムJsonDeserializerを作成する必要がある場合もありますが、これはそのような場合ではありません。メッセージの送信またはエラーは非常に標準的な方法であり、適切なデータ構造を使用すると、GSONはこのような単純なユースケースを直接処理できます。

JSONスキーマを制御できる場合

代わりに、次のようなものを検討してください。

{
  "message": {
    "foo": "Hello World",
    "bar": 3
  },
  "error": null;
}

{
  "message": null,
  "error": {
    "publicMsg": "Something bad happened",
    "msg": "you forgot requesting some parameter"
  }
}

可能な場合はprovidesDummyオブジェクトであるクリーンラッパークラスを定義できることに注意してください。

public class JsonResponse {
  private Dummy message;
  private RequestError error;

  public boolean hasError() { return error != null; }
  public Dummy getDummy() {
    Preconditions.checkState(!hasError());
    return message;
  }
  public RequestError getError() {
    Preconditions.checkState(hasError());
    return error;
  }
}

既存のJSONスキーマを処理する必要がある場合

スキーマを再構築できない場合は、解析クラスを再構築する必要があります。次のようになります。

public class JsonResponse {
  private String foo;
  private int bar;

  private RequestError error;

  public boolean hasError() { return error != null; }
  public Dummy getDummy() {
    Preconditions.checkState(!hasError());
    return new Dummy(foo, bar);
  }
  public RequestError getError() {
    Preconditions.checkState(hasError());
    return error;
  }
}

これはスキーマを修正するよりも望ましくありませんが、どちらの方法でも同じ一般的なAPIを取得します。hasError()を呼び出してリクエストが成功したかどうかを確認してから、getDummy()またはgetError()を呼び出します。必要です。他のメソッド(エラーを受信したときのgetDummy()など)の呼び出しはフェイルファストになります。

2
dimo414