NEW BOOK! SwiftUI Fundamentals: The essential guide to SwiftUI core concepts and APIs. Learn more ...NEW BOOK! SwiftUI Fundamentals:Master SwiftUI core concepts and APIs. Learn more...

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 and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35