如何使用Gson处理具有相同属性名称的不同数据类型?

13

我正在使用Gson在Java中编写RSS订阅源解析器。我将RSS的XML转换为JSON,然后使用Gson将JSON反序列化为Java POJO(有些迂回,但有其原因)。就反序列化第一条订阅源(下面的BBC)而言,一切都正常运行,但对于下面列出的第二个订阅源(NPR),我开始收到异常。

我认为我已经确定了问题,但不确定如何解决:


这两个RSS订阅源(例如)存在问题:

  1. http://feeds.bbci.co.uk/news/rss.xml
  2. http://www.npr.org/rss/rss.php?id=1001

对于这些不同的RSS订阅源,称为“guid”的字段被返回为a)具有2个字段的对象(如BBC RSS Feed)或b)字符串(如NPR RSS Feed)。

以下是相关JSON的某些改写版本:

BBC RSS Feed

// is returning 'guid' as an object
"item" : 
[
    {
        // omitted other fields for brevity
        "guid" : {
            "isPermalink" : false,
            "content" : "http:\/\/www.bbc.co.uk\/news\/uk-england-33745057"
        },
    },
    {
        // ...
    }
]

NPR RSS订阅

// is returning 'guid' as a string
"item" : 
[
    {
      // omitted other fields for brevity
      "guid" : "http:\/\/www.npr.org\/sections\/thetwo-way\/2015\/07\/31\/428188125\/chimps-in-habeas-corpus-case-will-no-longer-be-used-for-research?utm_medium=RSS&utm_campaign=news"
    },
    {
      // ...
    }
]

我用Java进行建模,代码如下:

// RSSFeedItem.java
private Guid guid;

// GUID.java
private boolean isPermalink;
private String content;
所以在这种情况下,调用它可以完美地运行。
Gson gson = new Gson();
RssFeed rssFeed = gson.fromJson(jsonData, RssFeed.class);

对于BBC RSS源,一切正常,但在处理NPR RSS源时出现异常。

导致我得出这是类型错误的具体错误是以下内容(在尝试反序列化NPR RSS源时):

Severe:    com.google.gson.JsonSyntaxException: java.lang.IllegalStateException:
           Expected BEGIN_OBJECT but was STRING at line 1 column 673 path
           $.rss.channel.item[0].guid

总之,谈正题:我该如何处理Gson中可能返回不同数据类型的字段的情况? 我猜可能有些技巧或注释可以帮助我解决这个问题,但我不确定,在查看了Gson文档后,没有找到一个可用的答案。


4个回答

10

你可以使用 TypeAdapter。其思想是仅在不同情况(字符串或对象)之间进行选择,并委托实际反序列化过程。

注册工厂:

public class RSSFeedItem {

    @JsonAdapter(GuidAdapterFactory.class)
    private Guid guid;
}

创建适配器的代码:

public class GuidAdapterFactory implements TypeAdapterFactory {

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        return (TypeAdapter<T>) new GuidAdapter(gson);
    }
}

决定如何处理guid的方法:

public class GuidAdapter extends TypeAdapter<Guid> {

    private final Gson gson;

    public GuidAdapter(Gson gson) {
        this.gson = gson;
    }

    @Override
    public void write(JsonWriter jsonWriter, Guid guid) throws IOException {
        throw new RuntimeException("Not implemented");
    }

    @Override
    public Guid read(JsonReader jsonReader) throws IOException {
        switch (jsonReader.peek()) {
            case STRING:
                // only a String, create the object
                return new Guid(jsonReader.nextString(), true);

            case BEGIN_OBJECT:
                // full object, forward to Gson
                return gson.fromJson(jsonReader, Guid.class);

            default:
                throw new RuntimeException("Expected object or string, not " + jsonReader.peek());
        }
    }
}

几点说明:

  • 这仅仅是因为适配器已经使用属性进行了注册才能实现。如果全局注册会触发递归调用,当真正的反序列化被委派时。

  • 工厂只需要是因为我们需要引用 Gson 对象,否则我们可以直接注册适配器类。

  • 我相信一个 TypeAdapter 比一个 Deserializer 更高效,因为它不需要构建一个 JsonElement 树,尽管在这种情况下差异可能可以忽略不计。


6

我的回答是利用类层次结构。

abstract class Guid {
    private boolean isPermalink;
    private String content;
    // getters and setters omitted
}

class GuidObject extends Guid {} 
class GuidString extends Guid {}

class RssFeedItem {
    // super class to receive instances of sub classes
    private Guid guid; 
}

并为 Guid 注册反序列化器:

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Guid.class, new JsonDeserializer<Guid>() {
        @Override
        public Guid deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
            // Dispatch based on the type of json
            if (json.isJsonObject()) {
                // If it's an object, it's essential we deserialize
                // into a sub class, otherwise we'll have an infinite loop
                return context.deserialize(json, GuidObject.class);
            } else if (json.isJsonPrimitive()) {
                // Primitive is easy, just set the most
                // meaningful field. We can also use GuidObject here
                // But better to keep it clear.
                Guid guid = new GuidString();
                guid.setContent(json.getAsString());
                return guid;
            }
            // Cannot parse, throw exception
            throw new JsonParseException("Expected Json Object or Primitive, was " + json + ".");
        }
    });

通过这种方式,您可以处理更复杂的JSON对象,并根据您喜欢的任何标准进行调度。


3
将其作为对象类而非其他类类型,并根据调用进行类型转换。
// RSSFeedItem.java
private Object guid;

这是一个好建议,因为它可以防止崩溃,但我最终得到了一个 LinkedTreeMap,你有没有一种简单的方法将 Map 转换为我的真实对象?否则,我仍然需要使用反序列化器来避免遍历树以填充我的对象层次结构。 - Nick Cardoso
尝试在此处获取请求和注释中反序列化后的对象类型 @NickCardoso - Sreemat
我有一个无关的修复,稍后会发布。你无法获取对象的类型,Gson也无法知道。如果将属性设置为Object,则会创建默认的LinkedTreeMap,这实际上是将JsonElement展平为映射。 - Nick Cardoso
在序列化时,请将其作为相关类型进行处理,而在反序列化时,请将其作为对象进行处理。@NickCardoso - Sreemat
这不是我正在序列化的自己的数据,所以那不是一个解决方案。 - Nick Cardoso

2

这是我的示例代码,希望您会觉得它有用。

public <T> List<T> readData(InputStream inputStream, Class<T> clazz) throws Exception {        
            ArrayList<Object> arrayList = new ArrayList<>();            
            GsonBuilder gsonBuilder = new GsonBuilder();
            Gson gson = gsonBuilder.create();
            JsonReader jsonReader = new JsonReader(new InputStreamReader(inputStream, "UTF_8"));
            jsonReader.setLenient(true);
            JsonToken jsonToken = jsonReader.peek();
            switch (jsonToken) {
                case BEGIN_ARRAY:
                    jsonReader.beginArray();
                    while (jsonReader.hasNext()) {
                        arrayList.add(gson.fromJson(jsonReader, clazz));
                    }
                    jsonReader.endArray();
                    break;
                case BEGIN_OBJECT:
                    T data = clazz.cast(gson.fromJson(jsonReader, clazz));
                    arrayList.add(data);
                    break;
                case NUMBER:
                    Integer number = Integer.parseInt(jsonReader.nextString());
                    arrayList.add(number);
                    break;
                default:
                    jsonReader.close();
                    inputStream.close();
                    return Collections.emptyList();
            }
            jsonReader.close();
            inputStream.close();
            return (List<T>) arrayList;        
    }

另一个是在Streams.java中的parseRecursive(你可以使用谷歌搜索)如下:

private static JsonElement parseRecursive(JsonReader reader)
            throws IOException {
        switch (reader.peek()) {
        case STRING:
            return new JsonPrimitive(reader.nextString());
        case NUMBER:
            String number = reader.nextString();
            return new JsonPrimitive(JsonPrimitive.stringToNumber(number));
        case BOOLEAN:
            return new JsonPrimitive(reader.nextBoolean());
        case NULL:
            reader.nextNull();
            return JsonNull.createJsonNull();
        case BEGIN_ARRAY:
            JsonArray array = new JsonArray();
            reader.beginArray();
            while (reader.hasNext()) {
                array.add(parseRecursive(reader));
            }
            reader.endArray();
            return array;
        case BEGIN_OBJECT:
            JsonObject object = new JsonObject();
            reader.beginObject();
            while (reader.hasNext()) {
                object.add(reader.nextName(), parseRecursive(reader));
            }
            reader.endObject();
            return object;
        case END_DOCUMENT:
        case NAME:
        case END_OBJECT:
        case END_ARRAY:
        default:
            throw new IllegalArgumentException();
        }
    }

更新:您还可以参考Streams类(gson-2.3.1.jar)中的parse(JsonReader reader)方法。

就像这样

JsonElement jsonElement = Streams.parse(jsonReader);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接