Search code examples
javajsonjson-deserializationmoshi

How to deserialize different object properties into a common class with Moshi?


I started writing the Java library Brockman to parse a JSON response similar to this example using Moshi. However, the format requires to generalize a bit for the stream objects.

The video stream excerpt:

{
    "slug": "hd-native",
    "display": "Saal 1 FullHD Video",
    "type": "video",
    "isTranslated": false,
    "videoSize": [
        1920,
        1080
    ],
    "urls": {
        "webm": {
            "display": "WebM",
            "tech": "1920x1080, VP8+Vorbis in WebM, 2.8 MBit/s",
            "url": "http://example.com/s1_native_hd.webm"
        },
        "hls": {
            "display": "HLS",
            "tech": "1920x1080, h264+AAC im MPEG-TS-Container via HTTP",
            "url": "http://example.com/hls/s1_native_hd.m3u8"
        }
    }
}

The audio stream excerpt:

{
    "slug": "audio-native",
    "display": "Saal 1 Audio",
    "type": "audio",
    "isTranslated": false,
    "videoSize": null,
    "urls": {
        "mp3": {
            "display": "MP3",
            "tech": "MP3-Audio, 96 kBit/s",
            "url": "http://example.com/s1_native.mp3"
        },
        "opus": {
            "display": "Opus",
            "tech": "Opus-Audio, 64 kBit/s",
            "url": "http://example.com/s1_native.opus"
        }
    }
}

The content of the urls = {} object differs dependent on whether the stream type is video or audio as one can see from the examples above.

Currently, there are only the models Mp3 and Opus which are identical by their properties. I would like to substitute them with a Format class which could also act as a replacement for the missing Webm and Hls classes. How can I actually map the different fields of the Urls object into the Format class?

public class Format {

    public final String display;
    public final String tech;
    public final String url;

    public Format(String display, String tech, String url) {
        this.display = display;
        this.tech = tech;
        this.url = url;
    }
}

I could imagine that the Stream class would look something like this:

public class Stream {

    public final String display;
    public final boolean isTranslated;
    public final String slug;
    public final String type;
    public final VideoSize videoSize;
    public final List<Format> urls; // How to map into List<Format>?

// ...

Solution

  • I came up with the following:

    public class StreamAdapter {
    
        @ToJson
        public String toJson(Stream stream) throws Exception {
            throw new UnsupportedOperationException("Not yet implemented.");
        }
    
        @FromJson
        public Stream fromJson(StreamJson streamJson) throws Exception {
            String slug = streamJson.slug;
            String display = streamJson.display;
            boolean isTranslated = streamJson.isTranslated;
            Stream.TYPE type = streamJson.type;
            VideoSize videoSize = streamJson.videoSize;
            Map<String, Object> urlsJson = streamJson.urls;
            List<Url> urls = getUrls(urlsJson);
            return new Stream(display, isTranslated, slug, type, videoSize, urls);
        }
    
        private List<Url> getUrls(Map<String, Object> urlsJson) {
            Set<String> urlTypes = urlsJson.keySet();
            List<Url> urls = new ArrayList<Url>(urlTypes.size());
            for (String urlType : urlTypes) {
                @SuppressWarnings("unchecked")
                Map<String, String> urlProperties = (Map<String, String>) urlsJson.get(urlType);
                urls.add(getUrl(urlType, urlProperties));
            }
            return urls;
        }
    
        private Url getUrl(String urlType, Map<String, String> properties) {
            Url.TYPE type = new UrlTypeAdapter().fromJson(urlType);
            String display = properties.get("display");
            String tech = properties.get("tech");
            String url = properties.get("url");
            return new Url(type, display, tech, url);
        }
    
        private static final class StreamJson {
            String slug;
            String display;
            boolean isTranslated;
            Stream.TYPE type;
            VideoSize videoSize;
            Map<String, Object> urls;
        }
    
    }
    

    Note that I use an Url class now instead of the Format class mentioned in the question. Please find more details here. If you have ideas on how to improve this you are welcome to comment here or create an issue/pull request on the repository.