Shadowman
Shadowman

Reputation: 12039

JAX-RS/Jackson -- Deserialize JSON with Unknown Root Element Name?

I am writing a RESTeasy Proxy Client to consume Apple's API for retrieving their iTunes category list. When you query for information about a given category , for example with this URL:

https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/genres?id=1420

...you get a JSON response that looks like this:

{  
   "1420":{  
      "name":"Self-Help",
      "id":"1420",
      "url":"https://itunes.apple.com/us/genre/podcasts-health-self-help/id1420?mt=2",
      "rssUrls":{  
         "topVideoPodcastEpisodes":"https://itunes.apple.com/us/rss/topvideopodcastepisodes/genre=1420/json",
         "topAudioPodcasts":"https://itunes.apple.com/us/rss/topaudiopodcasts/genre=1420/json",
         "topVideoPodcasts":"https://itunes.apple.com/us/rss/topvideopodcasts/genre=1420/json",
         "topPodcasts":"https://itunes.apple.com/us/rss/toppodcasts/genre=1420/json",
         "topAudioPodcastEpisodes":"https://itunes.apple.com/us/rss/topaudiopodcastepisodes/genre=1420/json",
         "topPodcastEpisodes":"https://itunes.apple.com/us/rss/toppodcastepisodes/genre=1420/json"
      },
      "chartUrls":{  
         "videoPodcastEpisodes":"https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/charts?cc=us&g=1420&name=VideoPodcastEpisodes",
         "podcasts":"https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/charts?cc=us&g=1420&name=Podcasts",
         "audioPodcastEpisodes":"https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/charts?cc=us&g=1420&name=AudioPodcastEpisodes",
         "audioPodcasts":"https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/charts?cc=us&g=1420&name=AudioPodcasts",
         "podcastEpisodes":"https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/charts?cc=us&g=1420&name=PodcastEpisodes",
         "videoPodcasts":"https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/charts?cc=us&g=1420&name=VideoPodcasts"
      }
   }
}

I am trying to map this JSON response to a Java object using JAXB and Jackson. However, the "1420" root element name seems to be causing a problem, as I get the following exception when calling my client:

Unrecognized field "1420" (class foo.bar.ITunesCategoryList), not marked as ignorable

My JAXB class looks like this:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class ITunesCategory implements TransferObject {

    private static final long serialVersionUID = 3443545925023804457L;

    @XmlElement(name = "id")
    @JsonProperty("id")
    private String identifier = null;

    @XmlElement
    private String name = null;

    @XmlElementWrapper(name = "subgenres")
    private List<ITunesCategory> subcategories = new ArrayList<ITunesCategory>();

    ...
}

I've even tried creating a wrapper class since the search could result in more than one category being returned. It looks like this:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class ITunesCategoryList implements TransferObject {

    private static final long serialVersionUID = 3303125979016445238L;

    @XmlElement
    private List<ITunesCategory> categories = new ArrayList<ITunesCategory>();

    ...
}

However, regardless of which class I specify as my return type, I get the same error because the category identifier is the root element name of the JSON object.

Is there any way to tell JAXB/Jackson/JAX-RS/RESTeasy to ignore the root element name and just map the underlying object to Java? There is no way for me to know the root element name at develop/compile time, since it corresponds directly to the results returned by the search. Is there anything that can be done to get around this exception? Thanks for any help you can give!

Upvotes: 4

Views: 2813

Answers (1)

Paul Samsotha
Paul Samsotha

Reputation: 208944

I couldn't find much on dynamically ignoring the root, at least not anything that would be suitable in a JAX-RS environment. The only thing I could think is to write a custom deserializer, and just skip the root node. Something like

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;

public abstract class IHateRootElemsJsonDeserializer<T> extends JsonDeserializer<T> {

    private final ObjectMapper mapper = new ObjectMapper();
    private final Class<T> cls;

    public IHateRootElemsJsonDeserializer(Class<T> cls) {
        this.cls = cls;
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext dc) 
            throws IOException, JsonProcessingException {
       JsonNode rootNode = jp.getCodec().readTree(jp);
       Map.Entry<String,JsonNode> field = rootNode.fields().next();
       JsonNode node = field.getValue();
       T result = mapper.convertValue(node, cls);
       return result;
    }  
}

Then just extend it with a concrete type.

public class GenreDeserializer extends IHateRootElemsJsonDeserializer<Genre>{

    public GenreDeserializer() {
        super(Genre.class);
    }
}

Here's a test using the exact JSON you provided above

public class Test {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        GenreDeserializer deserializer = new GenreDeserializer();
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Genre.class, deserializer);
        mapper.registerModule(module);

        Genre genre = mapper.readValue(JSON_FILE, Genre.class);
        System.out.println(genre);

        genre = mapper.readValue(JSON_FILE, Genre.class);
        System.out.println(genre);
    }

    static final File JSON_FILE = new File("json.json");
}

The model

public class Genre {

    public String id;
    public String name;
    public String url;
    public RssUrls rssUrls;
    public ChartUrls chartUrls;

    @Override
    public String toString() {
        return "Category{" + "id=" + id + ", name=" 
                + name + ", url=" + url + ", rssUrls=" + rssUrls + '}';
    }

    public static class RssUrls {
        public String topVideoPodcastEpisodes;
        public String topAudioPodcasts;
        public String topVideoPodcasts;
        public String topPodcasts;
        public String topAudioPodcastEpisodes;
        public String topPodcastEpisodes;

        @Override
        public String toString() {
            return "RssUrls{" + "topVideoPodcastEpisodes=" + topVideoPodcastEpisodes 
                    + ", topAudioPodcasts=" + topAudioPodcasts 
                    + ", topVideoPodcasts=" + topVideoPodcasts 
                    + ", topPodcasts=" + topPodcasts 
                    + ", topAudioPodcastEpisodes=" + topAudioPodcastEpisodes 
                    + ", topPodcastEpisodes=" + topPodcastEpisodes + '}';
        }

    }

    public static class ChartUrls {
        public String videoPodcastEpisodes;
        public String podcasts;
        public String audioPodcastEpisodes;
        public String audioPodcasts;
        public String podcastEpisodes;
        public String videoPodcasts;

        @Override
        public String toString() {
            return "ChatUrls{" + "videoPodcastEpisodes=" + videoPodcastEpisodes 
                    + ", podcasts=" + podcasts 
                    + ", audioPodcastEpisodes=" + audioPodcastEpisodes 
                    + ", audioPodcasts=" + audioPodcasts 
                    + ", podcastEpisodes=" + podcastEpisodes
                    + ", videoPodcasts=" + videoPodcasts + '}';
        }   
    }
}

To configure the ObjectMapper in JAX-RS, you can have a look at this post

Upvotes: 6

Related Questions