如何使用 Gson 处理具有相同属性名称的不同数据类型?
How to handle different data types with same attribute name with Gson?
我目前正在 Java 使用 Gson 编写 RSS 提要解析器。我正在将 RSS 的 XML 转换为 JSON,然后使用 Gson 将 JSON 反序列化为 Java POJO(有点迂回,但这是有原因的)。就下面列出的提要 #1 (BBC) 的反序列化而言,一切工作正常,但对于下面列出的提要 #2 (NPR) ,我开始抛出异常。
我想我已经确定了问题所在,但不确定如何解决它:
问题是由这两个 RSS 源引起的(例如):
对于这些不同的 RSS 提要,一个名为 "guid" 的字段被返回为 a) 一个具有 2 个字段的对象(如 BBC RSS Feed) 或 b) a string(如 NPR RSS Feed)。
这里是相关JSON的一些释义版本:
BBC RSS 订阅
// 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 的文档后我找不到现成的答案。
这是我的示例代码,希望对您有所帮助
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;
}
还有一个是parseRecursive
在Streams.java
(可以Google搜索)如下:
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
中的parse(JsonReader reader)
class (gson-2.3.1.jar)
像这样
JsonElement jsonElement = Streams.parse(jsonReader);
我的答案是利用 class 层次结构。
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 对象,并根据您喜欢的任何标准进行调度。
将其设为对象 Class 而不是其他 Class 根据调用类型和类型转换
// RSSFeedItem.java
private Object guid;
您可以使用 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
对象,否则我们可以直接注册适配器class.
我相信 TypeAdapter
比 Deserializer
更有效,因为它不需要构建 JsonElement
树,尽管在这种情况下存在差异可能可以忽略不计。
我目前正在 Java 使用 Gson 编写 RSS 提要解析器。我正在将 RSS 的 XML 转换为 JSON,然后使用 Gson 将 JSON 反序列化为 Java POJO(有点迂回,但这是有原因的)。就下面列出的提要 #1 (BBC) 的反序列化而言,一切工作正常,但对于下面列出的提要 #2 (NPR) ,我开始抛出异常。
我想我已经确定了问题所在,但不确定如何解决它:
问题是由这两个 RSS 源引起的(例如):
对于这些不同的 RSS 提要,一个名为 "guid" 的字段被返回为 a) 一个具有 2 个字段的对象(如 BBC RSS Feed) 或 b) a string(如 NPR RSS Feed)。
这里是相关JSON的一些释义版本:
BBC RSS 订阅
// 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 的文档后我找不到现成的答案。
这是我的示例代码,希望对您有所帮助
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;
}
还有一个是parseRecursive
在Streams.java
(可以Google搜索)如下:
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
中的parse(JsonReader reader)
class (gson-2.3.1.jar)
像这样
JsonElement jsonElement = Streams.parse(jsonReader);
我的答案是利用 class 层次结构。
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 对象,并根据您喜欢的任何标准进行调度。
将其设为对象 Class 而不是其他 Class 根据调用类型和类型转换
// RSSFeedItem.java
private Object guid;
您可以使用 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
对象,否则我们可以直接注册适配器class.我相信
TypeAdapter
比Deserializer
更有效,因为它不需要构建JsonElement
树,尽管在这种情况下存在差异可能可以忽略不计。