13

I'm currently writing an RSS feed parser in Java utilizing Gson. I'm converting the RSS' XML into JSON, and then subsequently using Gson to deserialize the JSON into Java POJOs (somewhat roundabout but there's a reason for it). Everything was working fine as far as deserializing for the feed #1 (BBC) listed below, but for the feed #2 (NPR) listed below, I started getting exceptions being thrown.

I think I have identified the problem, but I'm uncertain as to how to resolve it:


The issue is arising with these two RSS Feeds (for example):

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

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

Here's some paraphrased versions of the relevant 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'm modeling this in Java like this:

// RSSFeedItem.java
private Guid guid;

// GUID.java
private boolean isPermalink;
private String content;

So in this case, it works perfectly fine calling

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 the following (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 anyway, to the point: how can I handle this situation with Gson, where a field is being returned as potentially different data types? I'm guessing there might be some sort of trick or annotation I could use to this effect, but I'm not certain and after checking the documentation for Gson I couldn't find a readily available answer.

Steve Pierce
  • 322
  • 2
  • 9

4 Answers4

10

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

Register the 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 makes the decision how to handle the 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());
        }
    }
}

A few remarks :

  • It only works because the adapter is registered with an attribute. Registering it globally triggers a recursive call when the actual deserialization is delegated.

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

  • I believe a TypeAdapter is more efficient than a Deserializer because it does not need a JsonElement tree to be build, although in this case the difference is probably negligible.

bwt
  • 17,292
  • 1
  • 42
  • 60
6

My answer is to make use of 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'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 + ".");
        }
    });

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

xiaofeng.li
  • 8,237
  • 2
  • 23
  • 30
3

Make it as Object Class instead of Other Class Type and Type cast according to the call

// RSSFeedItem.java
private Object guid;
Sreemat
  • 616
  • 10
  • 28
  • This is a good suggestion in that it stops the crash, but I end up with a LinkedTreeMap, do you have a simple way to convert the Map to my real object? Otherwise I still have to use a deserializer to avoid traversing the tree to populate my object hierarchy – Nick Cardoso Jan 18 '17 at 08:52
  • Try to get the type of object after deserialize in both request and comment here @NickCardoso – Sreemat Jan 18 '17 at 10:11
  • I've got an unrelated fix I'll post later. You cannot get the type of the object, Gson has no way of knowing. If you set the attribute to Object it creates the default LinkedTreeMap which is effectively a JsonElement flattened into a map – Nick Cardoso Jan 18 '17 at 10:32
  • While serializing make as relevant type while desalinizing make it as object @NickCardoso – Sreemat Jan 18 '17 at 10:34
  • This isn't my own feed I'm serializing, so that isn't a solution – Nick Cardoso Jan 18 '17 at 10:40
2

Here is my sample code, hope you find it helpful

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 one is parseRecursive in Streams.java (you can Google search) as 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 Streams class (gson-2.3.1.jar)

Like this

JsonElement jsonElement = Streams.parse(jsonReader);
BNK
  • 23,994
  • 8
  • 77
  • 87