Подтвердить что ты не робот

Пусть Gson бросает исключения на неправильные типы

Я использую Gson внутри своих проектов для десериализации JSON-строк для Java-объектов. Если я сделаю запрос, я ожидаю, что на сервере будет четко определен ответ. Сервер либо вернет корректный ответ, который я ожидаю, либо он вернет мне (также определенный) объект ошибки.

Чтобы все было ясно: предположим, что у меня есть простой объект:

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

и объект Error следующим образом:

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 и bar равно 0! В документации Gson (fromJson) четко указано, что:

броски JsonSyntaxException - если json не является допустимым представлением для объект типа classOfT

поэтому я ожидал получить исключение JsonSyntaxException, если попытаюсь проанализировать второй ответ вроде этого:

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

поскольку Json не представляет объект Dummy, а объект ErrorHolder.

Итак, мой вопрос: есть ли способ, который Gson каким-то образом обнаруживает, и бросает мне исключение?

4b9b3361

Ответ 1

К сожалению, документация немного вводит в заблуждение.

Это вызовет только исключение, если у вашего класса было поле, тип которого не соответствовал тому, что находится в JSON, и даже тогда он делает некоторые сумасшедшие вещи, чтобы попытаться его исправить (преобразование int в JSON в String в вашем классе, например). Если в вашем POJO было что-то вроде поля Date, и он столкнулся с int в JSON, он бы выбросил его. Поля, которые присутствуют в JSON, но не в вашем POJO, игнорируются, поля, которые отсутствуют в JSON, но существуют в вашем POJO, установлены на null.

В настоящее время GSON не предоставляет механизма для какой-либо "строгой" десериализации, где у вас будет что-то вроде аннотации @Required для полей вашего POJO.

В вашем случае... Я бы просто расширил свой 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 требовал настраиваемого десериализатора as-is, вам придется идти немного дальше и либо передать объект 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;

    }
}

Выход:

This is foo
this is bar
This is foo
null
Exception in thread "main" com.google.gson.JsonParseException: Missing field in JSON: foo

Ответ 2

Я создал обновленную версию решения Брайана, которая обрабатывает вложенные объекты и имеет несколько других незначительных изменений. Код также включает более простой конструктор для создания объектов Gson, которые знают классы с полями, аннотированными 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 {
}
}

И утилита 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"
  }
}

Обратите внимание, что теперь вы можете определить класс чистой оболочки, который предоставляет Dummy объекты, когда это возможно:

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() при получении ошибки) будет неудачным.