Swift Tip: Codable Enums
If we have an enum wih associated types, the compiler can't automatically generate Codable
conformance yet. For example, let's create an enum that can be one of two values:
enum Either<A,B> {
case left(A)
case right(B)
}
If we use the latest Swift toolchain, we can conditionally make it conform to Codable
(see this gist), but even in the current Swift release we can make it work by adding Codable
constraints to our generic parameters:
enum Either<A: Codable, B: Codable> {
case left(A)
case right(B)
}
To encode, we create two CodingKeys
, and use those keys to tag the value before encoding it:
extension Either: Encodable {
enum CodingKeys: CodingKey {
case left
case right
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .left(let value):
try container.encode(value, forKey: .left)
case .right(let value):
try container.encode(value, forKey: .right)
}
}
}
To decode, we mirror the process. First, we try to decode using the .left
key. If that doesn't work, we catch the error, and try to decode using the .right
key. If that doesn't work either, the error will be propagated (just like a regular decoding error):
extension Either: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
let leftValue = try container.decode(A.self, forKey: .left)
self = .left(leftValue)
} catch {
let rightValue = try container.decode(B.self, forKey: .right)
self = .right(rightValue)
}
}
}
Now we can make use of our new Codable
conformance:
import Foundation
let values: [Either<String,Int>] = [
.left("Hi"),
.right(42)
]
let encoder = JSONEncoder()
let data = try! encoder.encode(values)
let str = String(data: data, encoding: .utf8)!
// [{"left":"Hi"},{"right":42}]
And we can see that we also can read it back:
let decoder = JSONDecoder()
let result = try! decoder.decode([Either<String,Int>].self, from: data)
// [.left("Hi"), .right(42)]
The technique above doesn't just work for the Either
type, but can be made to work for any enum with associated values, as long as all associated values are Codable
themselves. Just add a case to CodingKeys
for each case in your enum.
For more information on Codable
, see our book Advanced Swift, which has a full chapter on encoding and decoding.