Gson uses TypeAdapter or Json Deserializer to convert data from an error list to an empty list

牧云@^-^@ 提交于 2019-12-20 04:34:37

问题


Let's start from example:

If the data is correct, it should be ( the Beijing cities is empty )

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

Now I got a wrong data. ( the Beijing cities is null )

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

I am using the Retrofit2 ResponseBodyConverter ,the entity class:

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;

}

The data obtained after deserialization is like this:

but the data I need is like this:

In order to have better fault tolerance, when the data is list, I want to process it by myself.
First of all,I tried to use 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 is valid when the data is "", {}, and [],but data is null, it will not work.

Then I tried to use 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());
    }
}

In this way, no matter what the data is, it can work properly.We know that using TypeToken<List<?>> will give us the LinkedHashMap,So although TypeAdapter can work properly, but I don't know how to convert JsonReader to the List <?>.

So I wonder if there are other ways that I can handle the wrong list data? Or convert JsonReader to the List <?> data I want.


回答1:


Implementing custom deserialiser for a list is always a little bit tricky. I propose go to one step above and customise deserialiser for Response class which looks a little bit easier. Below you can see example app which handles all possible values and in case of table, deserialises it:

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
}

Above code for JSON payload with null:

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

prints:

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

For JSON payload with primitive:

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

prints:

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

For JSON payload with object:

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

prints:

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

And finally for valid JSON payload:

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

prints:

Response{code=200, msg='', data=[Data{value='Gson is the best'}, Data{value='Jackson is good as well'}]}



回答2:


I created new answer because it shows different approach how to solve this problem. It is not perfect but shows the idea how to create custom adapter with back reference to element adapter created by Gson object which we want to customise. If we want to have control on handling primitive-s and null-es we need to extend com.google.gson.TypeAdapter. Most of the code I got from com.google.gson.internal.bind.CollectionTypeAdapterFactory.Adapter class which is closed for modifications and extensions. Custom implementation looks like below:

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

Main difference is above implementation returns Collections.emptyList() for null token. We can handle other bad tokens before we start read collection. Below we can see how to register above adapter:

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

I've included all imports to clarify which types come from which packages. Let's test it with below JSON payload:

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

App prints:

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

At the end I need to remind about com.google.gson.internal.bind.ReflectiveTypeAdapterFactory which is used by default for POJO classes. If in JSON property is skipped, it will not invoke adapter so we need to set POJO property to default values by us:

private List<Data> cities = Collections.emptyList();



回答3:


I found the CollectionTypeAdapterFactory in Gson source code.I tried to modify it,it has been tested and it is useful.

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

In the source code the TypeAdapterRuntimeTypeWrapper is protected,We must make a copy.

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

How to use

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

prints:

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


来源:https://stackoverflow.com/questions/55085592/gson-uses-typeadapter-or-json-deserializer-to-convert-data-from-an-error-list-to

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!