I recently encountered a somewhat unusual Swift problem. I had a struct that looked something like this:

struct Something {
    let id: Int
    let someValue: String
}

I wanted to be able to construct one of these from a JSON response coming from a server. Obviously, I figured the easiest way to do this was to use Codable. So I did:

struct Something : Codable {
    let id: Int
    let someValue: String
}

let decoder = JSONDecoder()
let decodedSomething = try decoder.decode(Something.self, from: jsonResponse)

Unfortunately, I then realized that this server was using different keys, and as a result the call to decode would fail. Here’s an example server response:

{
    "id": 1,
    "sV": "foo"
}

So, to deal with that, I did this:

struct Something : Codable {
    let id: Int
    let someValue: String

    enum CodingKeys: String, CodingKey {
        case id
        case someValue = "sV"
    }
}

let decoder = JSONDecoder()
let decodedSomething = try decoder.decode(Something.self, from: jsonResponse)

And… everything was hunky dory.

Except… it wasn’t. I had to also store a JSON representation of these structs on disk, and retrieve them later. I really did not want to use the same JSON representation as the server, because it looks ugly, and makes debugging annoying. I wanted to have the on-disk representation be:

{
    "id": 1,
    "someValue": "foo"
}

At first, I wasn’t really able to find a way to do this. It then occurred to me that I’d likely have to implement encode(to encoder: Encoder) and it’s counterpart init(from decoder: Decoder) to do this, so I attempted to. Unfortunately, that led me to another problem. I’d need to have different CodingKeys enums for the server representation, and for my on-disk representation. But, even if I did have different CodingKeys, how would I know which one to use with which Encoder or Decoder? This also led me to a broader realization. Even if I tried to make my on-disk representation a Plist, instead of JSON, I’d still have to use the same CodingKeys, which was suboptimal.

So, I did what every good developer does, and googled for possible solutions to this problem, but did not find any. Then, I started reading the documentation hoping I’d find a way to solve this problem, and eventually came up with this solution:

let decoderTypeKey = CodingUserInfoKey(rawValue: "decoderType")!
let encoderTypeKey = CodingUserInfoKey(rawValue: "encoderType")!

class ServerJSONDecoder : JSONDecoder {
    static let decoderType = "ServerDecoder"

    override init() {
        super.init()
        userInfo = [decoderTypeKey: ServerJSONDecoder.decoderType]
    }
}

class ServerJSONEncoder : JSONEncoder {
    static let encoderType = "ServerEncoder"

    override init() {
        super.init()
        userInfo = [encoderTypeKey: ServerJSONEncoder.encoderType]
    }
}

struct Something : Codable {
    let id: Int
    let someValue: String

    init(id: Int, someValue: String) {
        self.id = id
        self.someValue = someValue
    }

    enum CodingKeys: String, CodingKey {
        case id
        case someValue
    }

    enum ServerCodingKeys: String, CodingKey {
        case id
        case someValue = "sV"
    }

    init(from decoder: Decoder) throws {
        if let decoderType = decoder.userInfo[decoderTypeKey] as? String {
            if decoderType == ServerJSONDecoder.decoderType {
                let container = try decoder.container(keyedBy: ServerCodingKeys.self)
                id = try container.decode(Int.self, forKey: .id)
                someValue = try container.decode(String.self, forKey: .someValue)
                return
            }
        }
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        someValue = try container.decode(String.self, forKey: .someValue)
    }

    func encode(to encoder: Encoder) throws {
        if let encoderType = encoder.userInfo[encoderTypeKey] as? String {
            if encoderType == ServerJSONEncoder.encoderType {
                var container = encoder.container(keyedBy: ServerCodingKeys.self)
                try container.encode(id, forKey: .id)
                try container.encode(someValue, forKey: .someValue)
                return
            }
        }
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(someValue, forKey: .someValue)
    }
}

let serverDecoder = ServerJSONDecoder()
let something = try serverDecoder.decode(Something.self, from: jsonResponse)
print(something)
// Output: Something(id: 1, someValue: "foo")

let serverEncoder = ServerJSONEncoder()
print(String.init(data: try serverEncoder.encode(something), encoding: .utf8)!)
// Output: {"id":1,"sV":"foo"}

let encoder = JSONEncoder()
print(String.init(data: try encoder.encode(something), encoding: .utf8)!)
// Output: {"id":1,"someValue":"foo"}

The basic idea here is that every Encoder has a userInfo dictionary, that you can put anything into. That same userInfo dictionary is made available at the time of encoding or decoding. I choose to put in the “type” of encoder in there, and use that as a mechanism to determine the type of Encoder or Decoder. This is what the ServerJSONEncoder and ServerJSONDecoder subclasses do.

Then, in the init(from decoder: Decoder) and encode(to encoder: Encoder) methods I look for the existence of that key in the userInfo dictionary. If it exists, and has a value I’m looking for, I use the appropriate CodingKeys to encode or decode.

And… everything is hunky dory again.

Except… it isn’t.

One problem with this solution is that it if I add another type of encoder/decoder, I now also have to update the struct itself, which is not good.

Second, there’s a whole bunch of repetition in the implementations of init(from decoder: Decoder) and encode(to encoder: Encoder). I haven’t yet found a way to avoid this duplication. The reason is because the type of container changes based on which CodingKey is used. So, I can’t pull container out of the if. If you have any ideas on how to avoid this duplication, or another way to solve this problem, please let me know! I’m @gopalkri on Twitter.