プロジェクト内で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"}}
Dummy
がfoo
で、null
が0の、一種のbar
オブジェクトを取得します。 Gsonのドキュメント(fromJson)には、次のことが明確に記載されています。
jsonSyntaxExceptionをスローします-jsonがclassOfT型のオブジェクトの有効な表現でない場合
したがって、次のように2番目の応答を解析しようとすると、JsonSyntaxExceptionが発生することが予想されました。
Dummy dummy = Gson.fromJson(secondResponse, Dummy.class);
jsonはダミーオブジェクトではなく、ErrorHolderオブジェクトを表すためです。
だから私の質問は:Gsonがどういうわけか間違ったタイプを検出し、私に例外をスローする方法はありますか?
残念ながら、ドキュメントは少し誤解を招く可能性があります。
クラスに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
ネストされたオブジェクトを処理し、他にいくつかの小さな変更を加えた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();
}
私は選択したソリューションのファンではありません。それは機能しますが、Gsonを使用する方法ではありません。 Gsonは、特定のJSONスキーマをオブジェクトにマップします。その逆も同様です。理想的には、使用しているJSONは整形式です(したがって、JSON形式を制御できる場合は、変更することを検討してください)が、そうでない場合は、受け取ると予想されるすべてのケースを処理するように解析オブジェクトを設計する必要があります。
カスタムJsonDeserializer
を作成する必要がある場合もありますが、これはそのような場合ではありません。メッセージの送信またはエラーは非常に標準的な方法であり、適切なデータ構造を使用すると、GSONはこのような単純なユースケースを直接処理できます。
代わりに、次のようなものを検討してください。
{
"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;
}
}
スキーマを再構築できない場合は、解析クラスを再構築する必要があります。次のようになります。
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()
など)の呼び出しはフェイルファストになります。