Давайте начнем с примера:
Если данные верны, это должно быть (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;
}
Данные, полученные после десериализации, выглядят так:
но данные мне нужны вот так:
Для лучшей отказоустойчивости, когда данные представлены в списке, я хочу обработать их самостоятельно.
Прежде всего, я попытался использовать 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
я хочу.
Я обнаружил 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'}]}]}
Я создал новый ответ, потому что он показывает другой подход, как решить эту проблему. Он не идеален, но показывает идею, как создать собственный адаптер с обратной ссылкой на элементный адаптер, созданный объектом 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();
Реализация пользовательского десериализатора для списка всегда немного сложнее. Я предлагаю перейти на один шаг выше и настроить десериализатор для класса 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'}]}
List<List<?>>
) и третий уровень ( List<List<List<?>>>
) , Я хочу использовать Gson для обработки всех неверных данных списка. Таким образом, мне не нужно каждый раз делать non-null
суждение по списку.
[null, {...}, ""]
и [[null], [{...}], [], ""]
и пропустить все null
, ""
, []
элементы внутри него. В этом случае пользовательский десериализатор для List
имеет больше смысла. В этом случае я постараюсь поиграть с этим и помочь найти лучшее решение. Было бы здорово, если бы вы могли создать несколько примеров того, как может выглядеть неправильная дата и чего вы хотели бы достичь на уровне Java
для данного набора данных.
Retrofit2
ResponseBodyConverter
, он все еще не решает мою проблему. Я изменю свой вопрос и продолжу изучать, как ее решить.Retrofit2
?