Using Codable with multiple representations

In most applications, a type has multiple Codable representations, depending on the context. For example:

  1. A document to store in a database.
  2. A JSON object to return from a REST API.
  3. A context to use when rendering an HTML template.

Is there a way to use Codable for multiple representations at once?

At the moment, I create separate types that have one Codable representation each, but I'm wondering if there's a better way.

1 Like

I create types for each representation which are then used to initialize the real type I want to use. These types are usually related through a protocol so I can use them generically. For instance, for types I get over the network, I create types that match the JSON exactly so I can take advantage of the automatic conformance and the initialize my real type with it, performing the transforms I need in the initializer.

1 Like

In my last project, I just created one type, which is encoded/decoded to/from both json and sqlite. I just named the database columns like the json keys to avoid having to deal with multiple coding keys. This has worked out well, but would not work if there were members that should only be encoded to one, not the other format.

Currently it is not possible to conform to the same protocol twice. But I think it should be possible. The usecase you mention is a very sensible one, this situation also appears when you want to implement simpler protocols than codable. The classical example:

protocol Combine {
    
    func combine(with rhs: Self) -> Self
    static var identity: Self { get }
}

You could have

extensions Int: Combine {
    func combine(with rhs: Int) -> Self {
        return self + rhs
    }
    static var identity: Int { return 0 }
}

And

extensions Int: Combine {
    func combine(with rhs: Int) -> Self {
        return self * rhs
    }
    static var identity: Int { return 1 }
}

But the we would get a compiler error saying int conforms twice to Combiner.

Interestingly enough Idris, an experimental programming language solves this problem with named implementation of interfaces http://docs.idris-lang.org/en/latest/tutorial/interfaces.html#named-implementations

We could imagine a similar solution where we name our implentstikns and pass in argument the name of the implementation that we want our function to use

If we have a utility reduce function

func reduce<C: Combiner>(array: [C]) -> C {
    return array.reduce(C.identity, { (res, c) in c.combine(with: res) })
}

We can use it for both addition and multiplication. But if we could name those implementations. We could have

extension Int@Add: Combiner {
    func combine(with rhs: Int) -> Int {
        return self + rhs
    }
    static var identity: Int { return 0 }
}

extension Int@Mult: Combiner {
    func combine(with rhs: Int) -> Int {
        return self * rhs
    }
    static var identity: Int { return 1 }
}

reduce<@Add>(array: [4,5,6]) // 15

reduce<@Mult>(array: [4,5,6]) // 120

Codable was purposefully designed so that this is possible to do. As @ahti mentions, if your representations between these types are close enough (or exactly equivalent), you shouldn't need to worry about the format as the encoders/decoders should be performing the translation for you. For instance, a type

struct Listing : Codable {
    var name: String
    var identifier: Int
}

should encode and decode from both

{
    "name": "..."
    "identifier": 42
}

and

<listing>
    <name>...</name> 
    <identifier>42</identifier>
</listing>

with no customization (given an XMLEncoder which produces this type of output, of course).

However, if you need multiple incompatible representations between formats, this is entirely possible within your init(from:) and encode(to:), e.g.:

struct Listing : Codable {
    // ...
    init(from decoder: Decoder) throws {
        if /* format is XML */ {
            // decode from my XML format
        } else {
            // default to a different representation
        }
    }

    func encode(to encoder: Encoder) throws {
        if /* format is XML */ { ... } else { ... }
    }
}

The question becomes "how do you know what format is being written to/read from?", and at the moment, there isn't one standard way to do this (though this is under consideration). Two possibilities at the moment:

  1. Check the type of encoder/decoder: this is simple and works if you know the type you expect to work with and can check for it. I wouldn't necessarily recommend this approach, though, because

    • The type isn't always known to you. For instance, checking decoder is JSONDecoder will always fail because when working with JSONDecoder, the decoder you get is of a private helper type
    • This prevents you from more easily working with multiple encoders/decoders of the same format type. Should you decode one way from JSONDecoder but a different way from AlternativeJSONDecoder? Likely the intention is to provide a single representation in JSON, so this isn't really forward-designed
  2. Inform your types by placing a marker in the userInfo dictionary: this is a more forward-facing approach as it lets you insert a marker however makes sense to your type regardless of the actual encoder/decoder:

    extension CodingUserInfoKey {
        static let knownFormatKey = CodingUserInfoKey(rawValue: "MyKnownFormatKey")!
    }
    
    let decoder = JSONDecoder()
    decoder.userInfo[.knownFormatKey] = "json"
    let listing = try decoder.decode(Listing.self, from: data)
    

    This allows your types to inspect decoder.userInfo and look for a marker that's meaningful to you:

    init(from decoder: Decoder) throws {
        let format = decoder.userInfo[.knownFormatKey] as? String
        switch format {
        case .some("json"): /* do JSON thing */
        case .some("xml"): /* do XML thing */
        default: /* unknown or nil; default to something else */
        }
    }
    

    The downside to this is that it isn't automatic.

We're considering different approaches for standardizing this process, but this is possible today.

13 Likes