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 struct
s 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.