Gson использует TypeAdapter или Json Deserializer для преобразования данных из списка ошибок в пустой список

1

Давайте начнем с примера:

Если данные верны, это должно быть (cities Beijing пусты)

{
   "code":200,
   "msg":"success",
   "data":[
      {
         "id":1,
         "name":"Beijing",
         "cities":[]
      },
      {
         "id":2,
         "name":"Guangdong",
         "cities":[
            {
               "id":1,
               "name":"Guangzhou"
            }
         ]
      }
   ]
}

Теперь я получил неправильные данные. (Beijing cities нулевые)

{
   "code":200,
   "msg":"success",
   "data":[
      {
         "id":1,
         "name":"Beijing",
         "cities":null
      },
      {
         "id":2,
         "name":"Guangdong",
         "cities":[
            {
               "id":1,
               "name":"Guangzhou"
            }
         ]
      }
   ]
}

Я использую Retrofit2 ResponseBodyConverter, класс сущности:

public class Result<T> {
    private int code;
    private String msg;
    private T data;

    // getters, setters
}

public class Province {
    private int id;
    private String name;
    private List<City> cities;

}

public class City {
    private int id;
    private String name;

}

Данные, полученные после десериализации, выглядят так:
Изображение 174551

но данные мне нужны вот так:
Изображение 174551

Для лучшей отказоустойчивости, когда данные представлены в списке, я хочу обработать их самостоятельно.
Прежде всего, я попытался использовать JsonDeserializer

Gson gson = new GsonBuilder()
              .serializeNulls()
              .registerTypeHierarchyAdapter(List.class, new GsonListAdapter())
              .create();

static class GsonListAdapter implements JsonDeserializer<List<?>> {
    @Override
    public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        if (json.isJsonArray()) {
            JsonArray array = json.getAsJsonArray();
            Type itemType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
            List list = new ArrayList<>();
            for (int i = 0; i < array.size(); i++) {
                JsonElement element = array.get(i);
                Object item = context.deserialize(element, itemType);
                list.add(item);
            }
            return list;
        } else {
            return Collections.EMPTY_LIST;
        }
    }
}

JsonDeserializer действителен, когда данные JsonDeserializer "", {} и [], но data JsonDeserializer null, он не будет работать.

Тогда я попытался использовать TypeAdapter

static class GsonListAdapter extends TypeAdapter<List<?>> {

    @Override
    public void write(JsonWriter out, List<?> value) throws IOException {
        out.value(String.valueOf(value));
    }

    @Override
    public List<?> read(JsonReader reader) throws IOException {
        if (reader.peek() != JsonToken.BEGIN_ARRAY) {
            reader.skipValue();
            return Collections.EMPTY_LIST;
        }
        return new Gson().fromJson(reader, new TypeToken<List<?>>() {}.getType());
    }
}

Таким образом, независимо от того, что это за data, они могут работать должным образом. TypeToken<List<?>> знаем, что использование TypeToken<List<?>> даст нам LinkedHashMap , Итак, хотя TypeAdapter может работать должным образом, но я не знаю, как конвертировать JsonReader в List<?>.

Поэтому мне интересно, есть ли другие способы, которыми я могу обработать неправильные данные списка? Или преобразовать JsonReader в List<?> data я хочу.

Теги:
gson

3 ответа

0

Я обнаружил CollectionTypeAdapterFactory в Gson коде Gson. Я попытался изменить его, он был протестирован и полезен.

public class CollectionTypeAdapterFactory implements TypeAdapterFactory {
    private final ConstructorConstructor constructorConstructor;

    public CollectionTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
        this.constructorConstructor = constructorConstructor;
    }

    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        Type type = typeToken.getType();

        Class<? super T> rawType = typeToken.getRawType();
        if (!Collection.class.isAssignableFrom(rawType)) {
            return null;
        }

        Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
        TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
        ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);

        @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter
                TypeAdapter<T> result = new Adapter(gson, elementType, elementTypeAdapter, constructor);
        return result;
    }

    private static final class Adapter<E> extends TypeAdapter<Collection<E>> {
        private final TypeAdapter<E> elementTypeAdapter;
        private final ObjectConstructor<? extends Collection<E>> constructor;

        public Adapter(Gson context, Type elementType,
                       TypeAdapter<E> elementTypeAdapter,
                       ObjectConstructor<? extends Collection<E>> constructor) {
            this.elementTypeAdapter =
                    new TypeAdapterRuntimeTypeWrapper<E>(context, elementTypeAdapter, elementType);
            this.constructor = constructor;
        }

        public Collection<E> read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) {
                in.nextNull();
                //In the source code is return null, I changed to return an empty collection
                return constructor.construct();
            }

            Collection<E> collection = constructor.construct();
            in.beginArray();
            while (in.hasNext()) {
                E instance = elementTypeAdapter.read(in);
                collection.add(instance);
            }
            in.endArray();
            return collection;
        }

        public void write(JsonWriter out, Collection<E> collection) throws IOException {
            if (collection == null) {
                out.nullValue();
                return;
            }

            out.beginArray();
            for (E element : collection) {
                elementTypeAdapter.write(out, element);
            }
            out.endArray();
        }
    }
}

В исходном коде TypeAdapterRuntimeTypeWrapper защищен, мы должны сделать копию.

  public class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
      private final Gson context;
      private final TypeAdapter<T> delegate;
      private final Type type;

      TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) {
          this.context = context;
          this.delegate = delegate;
          this.type = type;
      }

      @Override
      public T read(JsonReader in) throws IOException {
          return delegate.read(in);
      }

      @SuppressWarnings({"rawtypes", "unchecked"})
      @Override
      public void write(JsonWriter out, T value) throws IOException {
          TypeAdapter chosen = delegate;
          Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
          if (runtimeType != type) {
              TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType));
              if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
                  // The user registered a type adapter for the runtime type, so we will use that
                  chosen = runtimeTypeAdapter;
              } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
                  // The user registered a type adapter for Base class, so we prefer it over the
                  // reflective type adapter for the runtime type
                  chosen = delegate;
              } else {
                  // Use the type adapter for runtime type
                  chosen = runtimeTypeAdapter;
              }
          }
          chosen.write(out, value);
      }

      private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
          if (value != null
                  && (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) {
              type = value.getClass();
          }
          return type;
      }
  }

Как пользоваться

Gson gson = new GsonBuilder().serializeNulls()
           .registerTypeAdapterFactory(
             new CollectionTypeAdapterFactory(new ConstructorConstructor(new HashMap<>()))
             )
           .create();

Result<List<Province>> result = gson.fromJson(jsonStr, new TypeToken<Result<List<Province>>>() {}.getType());

печатает:

Result{code=200, msg='success', data=[Province{id=1, name='Beijing', cities=[]}, Province{id=2, name='Guangdong', cities=[City{id=1, name='Guangzhou'}]}]}
0

Я создал новый ответ, потому что он показывает другой подход, как решить эту проблему. Он не идеален, но показывает идею, как создать собственный адаптер с обратной ссылкой на элементный адаптер, созданный объектом Gson который мы хотим настроить. Если мы хотим иметь контроль над обработкой primitive и null -es нам нужно расширить com.google.gson.TypeAdapter. Большую часть кода я получил из класса com.google.gson.internal.bind.CollectionTypeAdapterFactory.Adapter который закрыт для изменений и расширений. Пользовательская реализация выглядит следующим образом:

class GenericListAdapter<E> extends TypeAdapter<List<E>> {

    private TypeAdapter<E> elementTypeAdapter;
    private final Supplier<List<E>> constructor;

    public GenericListAdapter() {
        this(ArrayList::new);
    }

    public GenericListAdapter(Supplier<List<E>> constructor) {
        this.constructor = constructor;
    }

    @Override
    public List<E> read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return Collections.emptyList();
        }

        List<E> collection = constructor.get();
        in.beginArray();
        while (in.hasNext()) {
            E instance = elementTypeAdapter.read(in);
            collection.add(instance);
        }
        in.endArray();
        return collection;
    }

    @Override
    public void write(JsonWriter out, List<E> collection) throws IOException {
        if (collection == null) {
            out.nullValue();
            return;
        }

        out.beginArray();
        for (E element : collection) {
            elementTypeAdapter.write(out, element);
        }
        out.endArray();
    }

    public void setElementTypeAdapter(TypeAdapter<E> elementTypeAdapter) {
        this.elementTypeAdapter = elementTypeAdapter;
    }
}

Основное отличие заключается в том, что реализация возвращает Collections.emptyList() для null токена. Мы можем обработать другие плохие токены до того, как начнем читать коллекцию. Ниже мы видим, как зарегистрироваться выше адаптера:

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.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        // create adapter
        GenericListAdapter<Data> dataGenericListAdapter = new GenericListAdapter<>();
        Type dataListType = new TypeToken<List<Data>>() {}.getType();
        Gson gson = new GsonBuilder()
                // register it for precise type
                .registerTypeAdapter(dataListType, dataGenericListAdapter)
                .create();

        // update with element adapter
        dataGenericListAdapter.setElementTypeAdapter(gson.getAdapter(Data.class));

        Response response = gson.fromJson(new FileReader(jsonFile), Response.class);
        System.out.println(response);
    }
}

Я включил все операции импорта, чтобы уточнить, какие типы поступают из каких пакетов. Давайте проверим это с помощью полезной нагрузки JSON ниже:

{
  "code": 200,
  "msg": "success",
  "data": [
    {
      "id": 1,
      "name": "Beijing",
      "cities": null
    },
    {
      "id": 2,
      "name": "Guangdong",
      "cities": [
        {
          "id": 1,
          "name": "Guangzhou"
        }
      ]
    }
  ]
}

Приложение печатает:

Response{code=200, msg='success', data=[Data{id=1, name='Beijing', cities=[]}, Data{id=2, name='Guangdong', cities=[Data{id=1, name='Guangzhou', cities=[]}]}]}

В конце мне нужно напомнить о com.google.gson.internal.bind.ReflectiveTypeAdapterFactory который используется по умолчанию для классов POJO. Если свойство JSON пропущено, оно не вызовет адаптер, поэтому нам нужно установить для свойства POJO значения по умолчанию:

private List<Data> cities = Collections.emptyList();
  • 0
    Это хороший метод. Но я использую Retrofit2 ResponseBodyConverter , он все еще не решает мою проблему. Я изменю свой вопрос и продолжу изучать, как ее решить.
  • 0
    @Choimroc, вы нашли способ решить эту проблему в Retrofit2 ?
Показать ещё 1 комментарий
0

Реализация пользовательского десериализатора для списка всегда немного сложнее. Я предлагаю перейти на один шаг выше и настроить десериализатор для класса Response который выглядит немного проще. Ниже вы можете увидеть пример приложения, которое обрабатывает все возможные значения, а в случае таблицы десериализует его:

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.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;

import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.List;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        Gson gson = new GsonBuilder()
                .registerTypeAdapter(Response.class, new ResponseErrorAwareJsonDeserializer())
                .create();

        Response response = gson.fromJson(new FileReader(jsonFile), Response.class);
        System.out.println(response);
    }
}

class ResponseErrorAwareJsonDeserializer implements JsonDeserializer<Response> {

    private final Type DATA_TYPE = new TypeToken<List<Data>>() {
    }.getType();

    @Override
    public Response deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        Response response = new Response();
        JsonObject jsonResponse = (JsonObject) json;
        response.setCode(jsonResponse.get("code").getAsInt());
        response.setMsg(jsonResponse.get("msg").getAsString());
        JsonElement dataElement = jsonResponse.get("data");
        if (dataElement.isJsonNull()) {
            System.out.println("Json data is null!");
        } else if (dataElement.isJsonPrimitive()) {
            System.out.println("Json data is primitive: " + dataElement.getAsString());
        } else if (dataElement.isJsonObject()) {
            System.out.println("Json data is an object: " + dataElement);
        } else if (dataElement.isJsonArray()) {
            List<Data> data = context.deserialize(dataElement, DATA_TYPE);
            response.setData(data);
        }

        return response;
    }
}

class Response {

    private int code;
    private String msg;
    private List<Data> data;

    // getters, setters, toString
}

class Data {

    private String value;

    // getters, setters, toString
}

Выше код для полезной нагрузки JSON с null:

{
  "code": 200,
  "msg": "",
  "data": null
}

печатает:

Json data is null!
Response{code=200, msg='', data=null}

Для полезной нагрузки JSON с primitive:

{
  "code": 500,
  "msg": "",
  "data": "Data[]"
}

печатает:

Json data is primitive: Data[]
Response{code=500, msg='', data=null}

Для полезной нагрузки JSON с object:

{
  "code": 500,
  "msg": "",
  "data": {
    "error": "Unknown"
  }
}

печатает:

Json data is an object: {"error":"Unknown"}
Response{code=500, msg='', data=null}

И, наконец, для корректной JSON:

{
  "code": 200,
  "msg": "",
  "data": [
    {
      "value": "Gson is the best"
    },
    {
      "value": "Jackson is good as well"
    }
  ]
}

печатает:

Response{code=200, msg='', data=[Data{value='Gson is the best'}, Data{value='Jackson is good as well'}]}
  • 0
    Спасибо за ваш ответ. Может быть, мой вопрос не достаточно полный. Хотя ваш метод может обрабатывать список первого уровня, однако, невозможно судить о вложенных списках, таких как второй уровень ( List<List<?>> ) и третий уровень ( List<List<List<?>>> ) , Я хочу использовать Gson для обработки всех неверных данных списка. Таким образом, мне не нужно каждый раз делать non-null суждение по списку.
  • 0
    @Choimroc, так что вы также хотите обработать ситуацию следующим образом: [null, {...}, ""] и [[null], [{...}], [], ""] и пропустить все null , "" , [] элементы внутри него. В этом случае пользовательский десериализатор для List имеет больше смысла. В этом случае я постараюсь поиграть с этим и помочь найти лучшее решение. Было бы здорово, если бы вы могли создать несколько примеров того, как может выглядеть неправильная дата и чего вы хотели бы достичь на уровне Java для данного набора данных.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню