January 23, 2018 · swift

Wacky JSON

In Swift, turning JSON content from a file or a server into structs or classes in code is much friendlier now that we have the Encodable and Decodable protocols. For simple cases you need to write almost no code and can quickly get on to something else.

For example, if you see this:

{
  "posts": [
      "this is a tweet maybe",
      "this is something political",
      "I'm angry about something",
      "I made something cool"
  ]
}

You can decode it with very little code:

struct PostPage: Decodable {
  let posts: [String]
}

let data = // Get this JSON from a file or the internet or your imaginination
let decoder = JSONDecoder()
let page = try docoder.decode(PostPage.self, from: data)

This is all great for most cases, but what happens when API-makers stop being polite and things get real?

What if your JSON looks like this:

{
  "posts": [
    "image",
    {
      "url": "http://somesite/image.jpg",
      "caption": "this is a horse" 
    },
    "text",
    "this is just text"
  ]
}

For some reason whoever designed this JSON structure felt it necessary to have an array of items where each item is not the same kind of thing. In this array of posts, there is a string which indicates a kind of post followed by a dictionary or a string or something which represents the data for that kind of post. Decoding this in swift, a language that loves type-safety and hates arrays of different types, will require a bit more code, but it can be done.

//: Playground - noun: a place where people can play

import Cocoa

let raw = """
{
    "posts" : [
        "image",
        { "url":"http://somesite/image.jpg", "caption":"this is a horse"},
        "text",
        "this is just some text"
    ]
}
""".data(using: .utf8)!

protocol Post: Decodable {
    // This is where any common info between the items would be required.
}

struct Image: Post {
    let url: URL
    let caption: String
}

struct Paragraph: Post {
    let text: String
}

struct PostsPage: Decodable {
    let posts: [Post]

    enum CodingKeys: String, CodingKey {
        case posts = "posts"
    }

    // An error to throw if the JSON changes later and we don't know
    // how to handle a new type of post.
    enum PostsErrors: Error {
        case unknownPostType
    }

    init(from decoder: Decoder) throws {
        // First dig out the array under the key posts
        let rawPage = try decoder.container(keyedBy: CodingKeys.self)
        var rawPosts = try rawPage.nestedUnkeyedContainer(forKey: .posts)

        var posts = [Post]()

        // each time you decode an element from an unkeyed container,
        // currentIndex is incremented
        while rawPosts.currentIndex < (rawPosts.count ?? 0) {
            // each element is preceded by a string which specifies it's type
            let type = try rawPosts.decode(String.self)
            switch type {
            case "image":
                let post = try rawPosts.decode(Image.self)
                print(post)
                posts.append(post)
            case "text":
                // text posts are just strings, so we have to decode the string
                // and then create a paragraph post from that.
                let text = try rawPosts.decode(String.self)
                posts.append(Paragraph(text: text))
            default:
                // for all types we don't know about that were maybe added
                throw PostsErrors.unknownPostType
            }
        }
        self.posts = posts
    }
}

let decoder = JSONDecoder()
let page = try? decoder.decode(PostsPage.self, from: raw)
print(page ?? "this failed")

Way more code than the simple case, but it does work. The end result is an array of different types of posts with their own info. Hopefully this sort of work won't be necessary for your projects, but now you know just in case.