Dealing with key-based polymorphic JSON in Swift Codables

I’ve been trying to use Swift’s Codable protocol with some data I wanted to decode over the wire. Codable makes it easy to serialize things in Swift. Unfortunately, the schema of the protocol I was using doesn’t cleanly map to something easily represented in Swift. It consists of a single object, with a single key, and the key’s name determining its value and type. For example, various JSON blobs:

{"Chat": {"message": "Hello world!"}}
{"Error": {"message": "Invalid request"}}
{"Hello": {"username": "alice", "version": "1.0"}}
/* there are more, but we'll stick with this set for the example */

How do we decode this cleanly?

Common parts

Just so we don’t have to repeat ourselves, let’s get our JSON blobs, JSON decoder, and other boilerplate out of the way:

import Foundation

/* Insert the definitions from each heading here */

let jsonChat = "{\"Chat\":{\"message\":\"Hello world!\"}}\r\n".data(using: .utf8)!
let jsonError = "{\"Error\":{\"message\":\"Oops!\"}}\r\n".data(using: .utf8)!
let jsonHello = "{\"Hello\":{\"username\":\"alice\",\"version\":\"1.0\",\"unknown\":1}}\r\n".data(using: .utf8)!

let decoder = JSONDecoder()

do {
    print(" ** Chat")
    let resultChat = try decoder.decode(Message.self, from: jsonChat)
    dump(resultChat)
    print(" ** Error")
    let resultError = try decoder.decode(Message.self, from: jsonError)
    dump(resultError)
    print(" ** Hello")
    let resultHello = try decoder.decode(Message.self, from: jsonHello)
    dump(resultHello)
} catch {
    dump(error)
}

We still need the structure or enum definitions here, but this basically provides us a type-safe, automatic way to turn JSON into structured data. It’ll be a lot easier than trying to do this by decoding into dictionaries.

FWIW, these approaches also seem to handle surplus keys and values on the associated objects by ignoring them.

First attempt: Struct with optionals

struct Message: Decodable {
    let Chat: Chat?
    let Error: Error?
    let Hello: Hello?

    struct Chat: Decodable {
        let message: String
    }

    struct Error: Decodable {
        let message: String
    }

    struct Hello: Decodable {
        let username: String
        let version: String
        let password: String?
    }
}

This is the “easiest” way, but it decodes into something clumsy to actually use. The output is:

 ** Chat
▿ test.Message
  ▿ Chat: Optional(test.Message.Chat(message: "Hello world!"))
    ▿ some: test.Message.Chat
      - message: "Hello world!"
  - Error: nil
  - Hello: nil
 ** Error
▿ test.Message
  - Chat: nil
  ▿ Error: Optional(test.Message.Error(message: "Oops!"))
    ▿ some: test.Message.Error
      - message: "Oops!"
  - Hello: nil
 ** Hello
▿ test.Message
  - Chat: nil
  - Error: nil
  ▿ Hello: Optional(test.Message.Hello(username: "alice", version: "1.0", password: nil))
    ▿ some: test.Message.Hello
      - username: "alice"
      - version: "1.0"
      - password: nil

This ends up with a lot of nil fields, and a lot of if let that’s not necessary. Swift actually has the tools to deal with this – enumerations can contain associated values, and can be pattern matched upon. This is crucial for dealing with the possibilities in a sane way.

Second attempt: simple enumeration

enum Message: Decodable { 
    case Hello(username: String, version: String, password: String?)
    case Chat(message: String)
    case Error(message: String)
}

This is really simple. Members of the object corresponding to the key are mapped as associated values. The output is something like:

 ** Chat
▿ test.Message.Chat
  ▿ Chat: (1 element)
    - message: "Hello world!"
 ** Error
▿ test.Message.Error
  ▿ Error: (1 element)
    - message: "Oops!"
 ** Hello
▿ test.Message.Hello
  ▿ Hello: (3 elements)
    - username: "alice"
    - version: "1.0"
    - password: nil

The nice thing is this can be pattern matched, and pattern matching means exhaustive case handling. It’ll make dealing with this less error-prone.

Note how the cases are upper-case to match the JSON. This isn’t very idiomatic, so to deal with cases where a Swift name diverges from the JSON name, you can add a CodingKey member to the enumeration (or structure – it works for keys of fields too!) like so:

enum Message: Decodable { 
    case hello(username: String, version: String, password: String?)
    case chat(message: String)
    case error(message: String)

    enum CodingKeys: String, CodingKey {
        case hello = "Hello"
        case chat = "Chat"
        case error = "Error"
    }
}

…and the automatic decoding will use these values, with output to match:

 ** Chat
▿ test.Message.chat
  ▿ chat: (1 element)
    - message: "Hello world!"
 ** Error
▿ test.Message.error
  ▿ error: (1 element)
    - message: "Oops!"
 ** Hello
▿ test.Message.hello
  ▿ hello: (3 elements)
    - username: "alice"
    - version: "1.0"
    - password: nil

Third attempt: complex enumeration

struct Chat: Decodable {
    let message: String
}

// NOTE: this change is to avoid overriding the stock Error type.
// I didn't do it in the previous example, sorry!
struct ProtocolError: Decodable {
    let message: String
}

struct Hello: Decodable {
    let username: String
    let version: String
    let password: String?
}

enum Message: Decodable {
    case hello(Hello)
    case chat(Chat)
    case error(ProtocolError)
    case unknown

    enum CodingKeys: String, CodingKey {
        case hello = "Hello"
        case chat = "Chat"
        case error = "Error"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let hello = try container.decodeIfPresent(Hello.self, forKey: .hello) {
            self = .hello(hello)
        } else if let chat = try container.decodeIfPresent(Chat.self, forKey: .chat) {
            self = .chat(chat)
        } else if let error = try container.decodeIfPresent(ProtocolError.self, forKey: .error) {
            self = .error(error)
        } else {
            self = .unknown
        }
    }
}

This is noticeably more complex, as I wanted to expand out from having associated values in the enum directly, to having dedicated structures as the single associated value instead. This would allow for more complex handling of the values, like methods or other such things associated with the structure. The output looks like:

 ** Chat
▿ test.Message.chat
  ▿ chat: (1 element)
    - message: "Hello world!"
 ** Error
▿ test.Message.error
  ▿ error: (1 element)
    - message: "Oops!"
 ** Hello
▿ test.Message.hello
  ▿ hello: (3 elements)
    - username: "alice"
    - version: "1.0"
    - password: nil

The biggest change is that we’re no longer doing automatic decoding (unfortunately, it seems Swift doesn’t handle the struct-as-associated-value case well), but adding a constructor that takes a keyed container, that Codable provides. This gives us the opportunity to look at the decoded JSON, and deserialize it ourselves. In the example, pretty much all we do is get the root object, and a series of if-let-else that optionally decodes if a key is present. If so, it’ll set the enum to the appropriate case with the contained object. Note that I added an .unknown case for the sake of exhaustive matching in the constructor since I was too lazy to throw here, which may not be the best solution. There’s almost certainly something better, this is just what I came to.

What would be nice is a hybrid approach: use associated values for some things, and structures for others. Unfortunately, I think this would still involve manual decoding, so it’s not that easy. If you have to involve that, then you might as well get the whole structure.

Leave a Reply

Your email address will not be published. Required fields are marked *