Codable conformance for Swift enums
Enums in Swift are a fundamental way to model a fixed set of choices or states in a type-safe manner. Adding Codable
conformance allows enums to be serialized and deserialized, enabling them to work seamlessly with data formats like JSON or property lists. This is particularly useful when interacting with APIs, saving app data, or exchanging information between systems.
# Automatic Codable conformance
In many cases, all we need to do is mark an enum as Codable
, and the compiler handles the rest. Before Swift 5.5, only enums conforming to RawRepresentable
could automatically conform to Codable
. Now, enums without raw values, including those with or without associated values, can also benefit from automatic Codable
synthesis if all associated types are themselves Codable
.
Let’s look at how automatic conformance works for different kinds of enums.
# Raw value enums
For enums with raw values, the encoded value corresponds to the raw value that represents it.
enum Direction: String, Codable {
case north
case south
case east
case west
}
let direction: Direction = .north
let jsonData = try JSONEncoder().encode(direction)
// Output: "north"
let jsonString = String(data: jsonData, encoding: .utf8)!
Decoding works the same way, the raw value is mapped back to the appropriate case.
# Enums without raw or associated values
Enums without associated values are encoded as an empty container, preserving the case name as the key.
enum Status: Codable {
case success
case failure
}
let status: Status = .success
let jsonData = try JSONEncoder().encode(status)
// Output: {"success":{}}
let jsonString = String(data: jsonData, encoding: .utf8)!
Decoding restores the enum case from the JSON key.
# Enums with associated values
For enums with associated values, each case is treated as a container with a key matching the case name. The associated values are encoded as nested key-value pairs within that container.
enum Command: Codable {
case load(key: String)
case store(key: String, value: Int)
}
let command: Command = .store(key: "exampleKey", value: 42)
let jsonData = try JSONEncoder().encode(command)
// Output: {"store":{"value":42,"key":"exampleKey"}}
let jsonString = String(data: jsonData, encoding: .utf8)!
For cases with unlabeled associated values, the compiler generates underscore-prefixed numeric keys, such as _0
, _1
, etc., based on their position.
enum Role: Codable {
case vipMember(String, Int)
}
let role: Role = .vipMember("1234", 5)
let jsonData = try JSONEncoder().encode(role)
// Output: {"vipMember":{"_0":"1234","_1":5}}
let jsonString = String(data: jsonData, encoding: .utf8)!
Optional associated values are treated as nil
if they are missing, which results in them being excluded from the encoded data.
# Customizing automatic conformance
# Customizing case names
By defining a CodingKeys
enum, we can map case names to custom keys during encoding and decoding.
enum Status: Codable {
case success
case failure(reason: String)
enum CodingKeys: String, CodingKey {
case success
case failure = "error"
}
}
This customization alters the JSON structure:
let status: Status = .failure(reason: "Invalid request")
let jsonData = try JSONEncoder().encode(status)
// Output: {"error":{"reason":"Invalid request"}}
let jsonString = String(data: jsonData, encoding: .utf8)!
# Customizing associated value keys
We can also customize the keys used for associated values by defining separate coding keys enums for each case. These coding keys enums have to be prefixed with the capitalized case name.
enum Command: Codable {
case load(key: String)
case store(key: String, value: Int)
enum StoreCodingKeys: String, CodingKey {
case key
case value = "data"
}
}
let command: Command = .store(key: "code", value: 123)
let jsonData = try JSONEncoder().encode(command)
// Output: {"store":{"key":"code","data":123}}
let jsonString = String(data: jsonData, encoding: .utf8)!
This approach is particularly useful when we need to align the encoded structure with external data formats.
# Excluding cases or values
Certain cases or associated values can be excluded by omitting them from the CodingKeys
declaration.
enum Event: Codable {
case start
case end(description: String, metadata: String = "")
enum EndCodingKeys: String, CodingKey {
case description
}
}
Values that are excluded must have a default value defined, if a Decodable
conformance should be synthesized.
# Custom Codable conformance
Automatic conformance doesn’t fit all use cases. For example, enums with overloaded case identifiers or complex encoding requirements might require custom implementations of encode(to:)
and init(from:)
.
enum Response: Codable {
case success(data: String)
case error(reason: String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .success(let data):
try container.encode(["status": "success", "data": data])
case .error(let reason):
try container.encode(["status": "error", "reason": reason])
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawData = try container.decode([String: String].self)
if let data = rawData["data"] {
self = .success(data: data)
} else if let reason = rawData["reason"] {
self = .error(reason: reason)
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Invalid data"
)
)
}
}
}
This allows a custom structure that doesn’t follow the default nested encoding format.
Codable
conformance makes enums in Swift even more powerful and versatile. While automatic synthesis covers many scenarios, customization and fully manual implementations offer the flexibility to handle complex requirements. With these tools, we can work confidently with serialized data, ensuring that our enums integrate seamlessly into real-world applications.
As someone who has worked extensively with Swift, I've gathered many insights over the years. I've compiled them in my book Swift Gems, which is packed with advanced tips and techniques to help intermediate and advanced Swift developers enhance their coding skills. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice to elevate your Swift development. Grab your copy of Swift Gems and let's explore the advanced techniques together.