let Gson throw exceptions on wrong types

2020-06-08 11:23发布

I use Gson inside my projects to deserialize JSON-Strings to Java-Objects. If I do a request, I expect a well-defined response from the Server. The Server will either return the well-defined response I expect, OR it will return me an (also defined) error Object.

To make things clear: suppose I have a simple Object like this:

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

and an Error Object like this:

class ErrorHolder{
   private RequestError error;
}

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

If I get a Server-response like

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

everything works as expected.

But if the response is like this

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

I'll get kind of an Dummy object where foo is null and bar is 0! The Gson documentation (fromJson) clearly states that:

throws JsonSyntaxException - if json is not a valid representation for an object of type classOfT

so i expected to get an JsonSyntaxException if I try to parse the second response like this:

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

because the Json doesn't represent a Dummy object, but an ErrorHolder Object.

So my question is: Is there a way, that Gson detects a wrong type somehow, and throws me an Exception?

3条回答
看我几分像从前
2楼-- · 2020-06-08 11:37

Unfortunately the documentation is slightly misleading there.

It will only throw the exception if your class had a field whose type didn't match what is in the JSON, and even then it does some crazy things to try and fix it (converting an int in the JSON to a String in your class for example). If you had something like a Date field in your POJO and it encountered an int in the JSON, it'd throw it. Fields that are present in the JSON but not in your POJO are silently ignored, fields that are missing in the JSON but exist in your POJO are set to null.

At present, GSON does not provide a mechanism for any sort of "strict" deserialization where you would have something like a @Required annotation for fields in your POJO.

In your case ... I'd simply expand my POJO to include an inner error object ... something like:

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.
}

Your other option is to write a custom deserializer that throws the exception if the JSON is the error such as:

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);
    }
} 

Edit to Add: Someone upvoted this recently and re-reading it I thought "Huh, you know, you could do this yourself and it might be handy".

Here's a re-usable deserializer and annotation that will do exactly what the OP wanted. The limitation is that if the POJO required a custom deserializer as-is, you'd have to go a little further and either pass in a Gson object in the constructor to deserialize to object itself or move the annotation checking out into a separate method and use it in your deserializer. You could also improve on the exception handling by creating your own exception and pass it to the JsonParseException so it can be detected via getCause() in the caller.

That all said, in the vast majority of cases, this will work:

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;

    }
}

Output:

This is foo
this is bar
This is foo
null
Exception in thread "main" com.google.gson.JsonParseException: Missing field in JSON: foo
查看更多
我想做一个坏孩纸
3楼-- · 2020-06-08 11:41

I created an updated version of Brian's solution that handles nested objects and has a couple of other minor changes. The code also includes a simpler builder to create Gson objects that are aware of classes with fields annotated with JsonRequired.

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 {
}
}

And the Reflection utility

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;
}
}

If you use Guice you could add something like this to your module to inject Gson objects

@Provides
@Singleton
static Gson provideGson() {
    return RequiredFieldAwareGsonBuilder.builder().withRequiredFieldAwareType(MyType1.class)
            .withRequiredFieldAwareType(MyType2.class).build();
}
查看更多
女痞
4楼-- · 2020-06-08 11:48

I'm not a fan of the selected solution. It works, but it's not the way to use Gson. Gson maps a particular JSON schema to an object and vice-versa. Ideally, the JSON you're using is well formed (so if you have control over the JSON format, consider changing it), but if not, you should design the parsing object to handle all cases you expect to receive.

Sometimes you do need to write a custom JsonDeserializer, but this is not one of those times. Sending a message or error is a very standard practice, and with the right data structure GSON can handle such a simple use case directly.

If you have control over the JSON schema

Consider something like this instead:

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

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

Notice you can now define a clean wrapper class that provides Dummy objects when possible:

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;
  }
}

If you have to deal with the existing JSON schema

If you can't restructure the schema, you have to restructure the parsing class, it would look something like this:

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;
  }
}

This is less desirable than fixing the schema, but you get the same general API either way - call hasError() to see if the request succeeded, then either call getDummy() or getError() as needed. Calls to the other method (e.g. getDummy() when you recieved an error) will fail-fast.

查看更多
登录 后发表回答