【问题标题】:Make GSON accept single objects where it expects arrays让 GSON 在它期望数组的地方接受单个对象
【发布时间】:2017-09-10 18:08:59
【问题描述】:

我有一堆模型类,它们的字段类型为List<X>,其中X 是其中之一(例如StringInteger,还有我自己的一些类型)。我正在使用 GSON 来解析这些模型的 JSON 表示。

我的问题是我正在处理的服务器(这是我无法控制的)以某种方式删除了单例数组并用包含的对象替换它们。

例如,而不是返回:

{
  "foo": [ "bar"],
  "bleh": [ { "some": "object" } ]
}

返回:

{
  "foo": "bar",
  "bleh": { "some": "object" }
}

现在假设 Java 模型类看起来像这样:

public class Model {
   private List<String> foo;
   private List<SomeObject> bleh;
}

目前这会导致 GSON 抛出异常,因为它在预期 BEGIN_ARRAY 的位置找到 BEGIN_STRINGBEGIN_OBJECT

对于数组或字符串列表,这很容易使用TypeAdapter&lt;List&lt;String&gt;&gt; 解决。但问题是我有许多不同元素类型的Lists,我不想为每种情况写一个单独的TypeAdapter。我也无法使用通用的TypeAdapter&lt;List&lt;?&gt;&gt;,因为在某些时候您需要知道类型。 那么是否有另一种方法可以将 GSON 配置为足够智能以将单个对象或值转换为数组/列表?或者换句话说,只是“假装”[] 存在于它期望找到它们的位置,尽管它们不存在?

【问题讨论】:

    标签: java json gson


    【解决方案1】:

    但问题是我有许多不同元素类型的列表,我不想为每种情况编写单独的 TypeAdapter。我也无法使用泛型 TypeAdapter>,因为在某些时候您需要知道类型。

    这就是类型适配器工厂的设计目的:您可以控制Gson 实例配置中的每种类型。

    final class AlwaysListTypeAdapterFactory<E>
            implements TypeAdapterFactory {
    
        // Gson can instantiate it itself
        private AlwaysListTypeAdapterFactory() {
        }
    
        @Override
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            // If it's not a List -- just delegate the job to Gson and let it pick the best type adapter itself
            if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
                return null;
            }
            // Resolving the list parameter type
            final Type elementType = resolveTypeArgument(typeToken.getType());
            @SuppressWarnings("unchecked")
            final TypeAdapter<E> elementTypeAdapter = (TypeAdapter<E>) gson.getAdapter(TypeToken.get(elementType));
            // Note that the always-list type adapter is made null-safe, so we don't have to check nulls ourselves
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> alwaysListTypeAdapter = (TypeAdapter<T>) new AlwaysListTypeAdapter<>(elementTypeAdapter).nullSafe();
            return alwaysListTypeAdapter;
        }
    
        private static Type resolveTypeArgument(final Type type) {
            // The given type is not parameterized?
            if ( !(type instanceof ParameterizedType) ) {
                // No, raw
                return Object.class;
            }
            final ParameterizedType parameterizedType = (ParameterizedType) type;
            return parameterizedType.getActualTypeArguments()[0];
        }
    
        private static final class AlwaysListTypeAdapter<E>
                extends TypeAdapter<List<E>> {
    
            private final TypeAdapter<E> elementTypeAdapter;
    
            private AlwaysListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) {
                this.elementTypeAdapter = elementTypeAdapter;
            }
    
            @Override
            public void write(final JsonWriter out, final List<E> list) {
                throw new UnsupportedOperationException();
            }
    
            @Override
            public List<E> read(final JsonReader in)
                    throws IOException {
                // This is where we detect the list "type"
                final List<E> list = new ArrayList<>();
                final JsonToken token = in.peek();
                switch ( token ) {
                case BEGIN_ARRAY:
                    // If it's a regular list, just consume [, <all elements>, and ]
                    in.beginArray();
                    while ( in.hasNext() ) {
                        list.add(elementTypeAdapter.read(in));
                    }
                    in.endArray();
                    break;
                case BEGIN_OBJECT:
                case STRING:
                case NUMBER:
                case BOOLEAN:
                    // An object or a primitive? Just add the current value to the result list
                    list.add(elementTypeAdapter.read(in));
                    break;
                case NULL:
                    throw new AssertionError("Must never happen: check if the type adapter configured with .nullSafe()");
                case NAME:
                case END_ARRAY:
                case END_OBJECT:
                case END_DOCUMENT:
                    throw new MalformedJsonException("Unexpected token: " + token);
                default:
                    throw new AssertionError("Must never happen: " + token);
                }
                return list;
            }
    
        }
    
    }
    

    现在您只需要告诉 Gson 哪些 字段格式不正确。 当然,您可以将整个 Gson 实例配置为接受此类列表,但使用 @JsonAdapter 注释让它更精确:

    final class Model {
    
        @JsonAdapter(AlwaysListTypeAdapterFactory.class)
        final List<String> foo = null;
    
        @JsonAdapter(AlwaysListTypeAdapterFactory.class)
        final List<SomeObject> bleh = null;
    
        @Override
        public String toString() {
            return "Model{" + "foo=" + foo + ", bleh=" + bleh + '}';
        }
    
    }
    
    final class SomeObject {
    
        final String some = null;
    
        @Override
        public String toString() {
            return "SomeObject{" + "some='" + some + '\'' + '}';
        }
    
    }
    

    测试数据:

    single.json

    {
        "foo": "bar",
        "bleh": {"some": "object"}
    }
    

    list.json

    {
        "foo": ["bar"],
        "bleh": [{"some": "object"}]
    }
    

    例子:

    private static final Gson gson = new Gson();
    
    public static void main(final String... args)
            throws IOException {
        for ( final String resource : ImmutableList.of("single.json", "list.json") ) {
            try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43412261.class, resource) ) {
                final Model model = gson.fromJson(jsonReader, Model.class);
                System.out.println(model);
            }
        }
    }
    

    还有输出:

    模型{foo=[bar], bleh=[SomeObject{some='object'}]}
    模型{foo=[bar], bleh=[SomeObject{some='object'}]}

    【讨论】:

    • 像魅力一样工作!谢谢!
    • 我明白了发生了什么,为什么AlwaysListTypeAdapterFactory 中有两个不同的泛型类型(即ET)?我看到EList-element 类型,但T 是什么? List-type 本身?
    • @Matthias 是的,T 类型参数由 Gson 定义和要求(它被定义为泛型方法类型参数)与类型标记“对齐”(参见 TypeAdapterFactory 接口)。 List 类型本身? -- 是的,完全正确。不幸的是,由于泛型在 Java 中的实现方式,上面有一个特殊的检查:!List.class.isAssignableFrom(typeToken.getRawType())(字面意思是“不是列表?”)。一旦我们确定它是一个列表,我们就会尽可能确定它的实际参数化类型,并结合两个类型适配器(自定义 for-list 一个和提供 for-element 一个)。
    • @Matthias 是的,它失败了。这取决于您的要求。如果您想始终将其序列化为列表(并且我相信它应该像这样工作),只需在创建类型适配器时使用 getDelegateAdapter(this, TypeToken.getParameterized(List.class, elementType))Gson 获取委托类型适配器,然后将工作委托给它。如果您希望类型适配器在列表大小为 1 时写入单个对象或在列表大小为 2 或更大时写入多个对象数组,则只需添加自定义逻辑(与 read 相同但相反)。
    • @Matthias 是的,快速浏览一下,它看起来就像我在第一个场景中描述的那样。
    【解决方案2】:

    您可以简单地编写自己的 JsonDeserializer 来检查您的 blehfoo 是 JsonObjects 还是 JsonArrays。

    检查 JsonElement 是数组还是对象:

    JsonElement element = ...;
    if (element.isJsonObject()) {
        //element is a JsonObject
    } else if (element.isJsonArray()) {
        //element is a JsonArray
    }
    

    【讨论】:

    • 这并不能真正解决我的问题。如果我不知道它是什么类型,我应该在 object case 中写什么。
    • Gson 会检查当前元素(可以是数组、对象、int 等)是数组还是对象。如果 foo 作为数组给出,您可以使用第二个检查,否则您可以检查 foo 是否为单个字符串。
    【解决方案3】:

    使用 GSON 库时,您可以只检查以下标记是对象还是数组。这当然需要您在解析 XML 时进行更精细的处理,但它允许您完全控制要从中获取什么。有时我们无法控制 XML,但它可能会派上用场。

    这是一个检查下一个标记是对象还是数组的例子,使用JsonReader类来解析文件:

    if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
        jsonReader.beginArray()
    } else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
        jsonReader.beginObject()
    }
    

    在数组/对象的末尾,您也可以这样做,但对于结束标记:

    if (jsonReader.peek() == JsonToken.END_ARRAY) {
        jsonReader.endArray()
    } else if (jsonReader.peek() == JsonToken.END_OBJECT) {
        jsonReader.endObject()
    }
    

    这样,您可以使用相同的代码(添加额外的检查,以验证您是在数组上还是在对象上)来解析对象数组或单个对象。

    【讨论】:

      【解决方案4】:

      我在使用供应商提供的 xml / json 时遇到了同样的问题 - 他们当然不会为我更改他们的代码 :) 在将它们更改为我自己的版本之前,我使用了一些网络资源 @987654321 @。我花了一些时间查看 gson 代码并找到了很多我想要访问的私有变量。所以,基本上我的自定义集合适配器所做的就是查看下一个元素是否是对象。如果没有,我们只需将读取委托给前一个适配器(我们已覆盖)。

      如果下一个元素是一个对象,我们使用 gson 来处理它。然后我们将其转换为一个对象的数组。使用 gson 将其写入字符串,然后将该字符串作为 JsonReader 传递给底层适配器。然后,这可以创建底层列表的实例并添加我们拥有的一个元素。

      这是 AdapterTypeFactory:

          public enum ListSingleObjectAdapterFactory implements TypeAdapterFactory {
          INSTANCE; // Josh Bloch's Enum singleton pattern
      
          @SuppressWarnings({ "unchecked", "rawtypes" })
          @Override
          public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
      
              Class<? super T> rawType = typeToken.getRawType();
              if (!Collection.class.isAssignableFrom(rawType)) {
                  return null;
              }
      
              TypeAdapter collectionAdapter = gson.getDelegateAdapter(this, typeToken);
              Class genericClass = (Class) ((ParameterizedType) typeToken.getType())
                      .getActualTypeArguments()[0];
                      return new SingleObjectOrCollectionAdapter(
                              gson, collectionAdapter, genericClass);
          }
      }
      

      那么我的类型适配器是:

      public class SingleObjectOrCollectionAdapter<T> extends TypeAdapter<Collection<T>> {
          private Class<T> adapterclass;
          private Gson gson;
          private TypeAdapter arrayTypeAdapter;
      
      
          public SingleObjectOrCollectionAdapter(Gson gson, TypeAdapter<T> collectionTypeAdapter, Class<T> componentType) {
              arrayTypeAdapter = collectionTypeAdapter;
              this.gson = gson;
              adapterclass = componentType;
          }
      
      
          @Override
          public Collection<T> read(JsonReader reader) throws IOException {
          Collection<T> collection;
              JsonReader myReader = reader;
              if (reader.peek() == JsonToken.BEGIN_OBJECT) {
                  T inning = gson.fromJson(reader, adapterclass);
                  String s = gson.toJson(new Object[]{inning});
                  myReader = new JsonReader(new StringReader(s));
      
              }
              collection = (Collection)arrayTypeAdapter.read( myReader );
      
              return collection;
          }
      
          @Override
          public void write(JsonWriter writer, Collection<T> value) throws IOException {
              arrayTypeAdapter.write(writer, value);
          }
      }
      

      最后,我们需要注册适配器工厂:

      GsonBuilder gb = new GsonBuilder().registerTypeAdapterFactory(ListSingleObjectAdapterFactory.INSTANCE);
      

      到目前为止,它似乎可以很好地处理单个和多个对象 - 尽管如果它需要在路上进行一些调整,我不会感到惊讶。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-28
        • 2020-03-31
        相关资源
        最近更新 更多