Swift's Codable protocol makes converting models to and from JSON straightforward. The compiler can synthesize most implementations, but APIs do not always use the same names and structure as your app. In those cases, a custom implementation gives you full control.

Renaming JSON Keys

Use a CodingKeys enum when a JSON key differs from the Swift property name:

struct User: Codable {
    let id: Int
    let displayName: String
    let isActive: Bool

    enum CodingKeys: String, CodingKey {
        case id
        case displayName = "display_name"
        case isActive = "is_active"
    }
}

The enum only needs to contain properties that should be encoded and decoded.

Decoding Nested Values

Sometimes the value you need is nested inside another object:

struct Article: Decodable {
    let title: String
    let authorName: String

    enum CodingKeys: String, CodingKey {
        case title
        case author
    }

    enum AuthorKeys: String, CodingKey {
        case name
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)

        let author = try container.nestedContainer(
            keyedBy: AuthorKeys.self,
            forKey: .author
        )
        authorName = try author.decode(String.self, forKey: .name)
    }
}

This keeps the app's model simple even when the API response is deeply nested.

Providing Default Values

Use decodeIfPresent when a key may be missing:

struct Settings: Decodable {
    let notificationsEnabled: Bool

    enum CodingKeys: String, CodingKey {
        case notificationsEnabled = "notifications_enabled"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        notificationsEnabled = try container.decodeIfPresent(
            Bool.self,
            forKey: .notificationsEnabled
        ) ?? true
    }
}

Encoding a Custom Structure

Custom encoding works in the opposite direction:

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(displayName, forKey: .displayName)
    try container.encode(isActive, forKey: .isActive)
}

Wrap up

Start with the synthesized Codable implementation and customize it only when the external data format requires it. CodingKeys, nested containers, and decodeIfPresent cover most real-world API responses.

Resources:

Keep reading

Read more

2 min read Swift Package: FilePicker Read article 5 min read AppDevCon 2025 Read article 6 min read Customizing macOS windows Read article

Share

Share this article

Share Bluesky Mastodon Twitter LinkedIn Facebook