How to handle different data types with the same attribute name with Gson?

I am currently writing an RSS feed parser in Java using Gson. I convert RSS-XML to JSON, and then later use Gson to deserialize JSON to Java POJO (somewhat roundabout, but there is a reason for this). Everything worked fine, like deserialization for channel # 1 ( BBC ) below, but for feed # 2 ( NPR ) below, I started getting exceptions from thrown.

I think I have identified the problem, but I'm not sure how to solve it:


A problem occurs with these two RSS feeds (for example):

For these different RSS feeds, a field called "guid" is returned as either: a) an object with two fields (as in the BBC RSS Feed), or b) a string (as in the NPR RSS feed ).

Here are some paraphrase versions of the corresponding 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 Feed

 // 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" }, { // ... } ] 

I model this in Java as follows:

 // RSSFeedItem.java private Guid guid; // GUID.java private boolean isPermalink; private String content; 

So in this case, it works fine when called

 Gson gson = new Gson(); RssFeed rssFeed = gson.fromJson(jsonData, RssFeed.class); 

for the BBC RSS feed , but it throws an exception when parsing the NPR RSS feed .

The specific error that led me to the conclusion that this is a type error was as follows (when trying to deserialize the NPR RSS feed ):

 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 

So, one way or another: how can I handle this situation with Gson, where the field is returned as potentially different data types? I suppose there might be some kind of trick or annotation that I could use to do this, but I'm not sure, and after checking the documentation for Gson, I could not find an easily accessible answer.

+7
java json gson deserialization json-deserialization
source share
4 answers

Here is my sample code, hope you find it useful

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

Another is parseRecursive in Streams.java (you can search on Google) as shown below:

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

UPDATE: you can also refer to parse(JsonReader reader) in class Streams (gson-2.3.1.jar)

Like this

 JsonElement jsonElement = Streams.parse(jsonReader); 
+2
source share

You can use TypeAdapter . The idea is to choose only between different cases (a string or an object) and delegate the actual deserialization.

Register Factory:

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

which creates the adapter:

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

which decides how to handle the pointer:

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

A few notes:

  • This only works because the adapter is registered with the attribute. Registering it worldwide causes a recursive call when actual deserialization is delegated.

  • Factory is needed only because we need a reference to the Gson object, otherwise we could register the adapter class directly.

  • I believe the TypeAdapter more efficient than the Deserializer because it does not require a JsonElement tree, although in this case the difference is probably not significant.

+3
source share

My answer is to use a class hierarchy.

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

And register a deserializer for 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 an object, it 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 + "."); } }); 

This way you can handle much more complex JSON objects and send based on whatever criteria you like.

+2
source share

Make it as an object class instead of another type and the type of the class selected according to the call

 // RSSFeedItem.java private Object guid; 
+2
source share

All Articles