---
name: swift-codable
description: "Codable JSON and property-list models: JSONDecoder, JSONEncoder, CodingKeys, custom init(from:)/encode(to:), nested containers, date, data and key strategies, heterogeneous and lossy arrays, missing-key defaults. Use when parsing API responses, remapping keys, flattening nested JSON, choosing decoding strategies, or wiring Codable into URLSession, SwiftData or UserDefaults."
metadata:
  source: multi-agent-pipeline
---

# Swift Codable

How to turn JSON and property lists into Swift values and back, for Swift 6.3
and iOS 26. The compiler writes most of the code; this skill is about the
places where it cannot, and about the boundaries where Codable meets
networking, SwiftData and `@AppStorage`.

## Basic conformance

When every stored property is itself Codable, the compiler synthesizes the
whole conformance.

```swift
struct Recipe: Codable {
    let id: Int
    let title: String
    let servings: Int
}

let recipe = try JSONDecoder().decode(Recipe.self, from: payload)
let bytes = try JSONEncoder().encode(recipe)
```

Pick the narrowest protocol:

- `Decodable` for data you only read, such as most API responses.
- `Encodable` for data you only send.
- `Codable` only when the type really travels both ways.

## Custom CodingKeys

A `CodingKeys` enum renames keys and still leaves decoding to the compiler.

```swift
struct Author: Decodable {
    let id: Int
    let penName: String
    let avatarURL: URL

    enum CodingKeys: String, CodingKey {
        case id
        case penName = "pen_name"
        case avatarURL = "avatar_url"
    }
}
```

Once the enum exists, it is the complete list of coded properties. A property
left out is not decoded or encoded at all, so it must have a default value or
be computed some other way.

## Custom decoding and encoding

Write `init(from:)` and `encode(to:)` when a value needs converting on the
way in or out.

```swift
struct Shipment: Codable {
    let reference: String
    let dispatchedAt: Date
    let labels: [String]

    enum CodingKeys: String, CodingKey {
        case reference, dispatchedAt = "dispatched_epoch", labels
    }

    init(from decoder: any Decoder) throws {
        let fields = try decoder.container(keyedBy: CodingKeys.self)
        reference = try fields.decode(String.self, forKey: .reference)
        let seconds = try fields.decode(Double.self, forKey: .dispatchedAt)
        dispatchedAt = Date(timeIntervalSince1970: seconds)
        labels = try fields.decodeIfPresent([String].self, forKey: .labels) ?? []
    }

    func encode(to encoder: any Encoder) throws {
        var fields = encoder.container(keyedBy: CodingKeys.self)
        try fields.encode(reference, forKey: .reference)
        try fields.encode(dispatchedAt.timeIntervalSince1970, forKey: .dispatchedAt)
        try fields.encode(labels, forKey: .labels)
    }
}
```

Doing the conversion here, instead of patching values after decoding, keeps
invalid instances from ever existing.

## Nested and flattened containers

JSON often nests what the app wants flat. `nestedContainer(keyedBy:forKey:)`
reaches into the inner object with its own key enum.

```swift
// { "name": "Harbour Cafe", "geo": { "latitude": 41.0, "longitude": 29.0 } }
struct Venue: Decodable {
    let name: String
    let latitude: Double
    let longitude: Double

    enum OuterKeys: String, CodingKey { case name, geo }
    enum GeoKeys: String, CodingKey { case latitude, longitude }

    init(from decoder: any Decoder) throws {
        let outer = try decoder.container(keyedBy: OuterKeys.self)
        name = try outer.decode(String.self, forKey: .name)
        let geo = try outer.nestedContainer(keyedBy: GeoKeys.self, forKey: .geo)
        latitude = try geo.decode(Double.self, forKey: .latitude)
        longitude = try geo.decode(Double.self, forKey: .longitude)
    }
}
```

Deeper structures chain further `nestedContainer` calls. An array inside a
keyed object comes from `nestedUnkeyedContainer(forKey:)`.

## Heterogeneous arrays

For a list whose elements differ by a `kind` field, model the element as an
enum with associated values. Read the discriminator first, then the payload it
selects.

```swift
enum FeedItem: Decodable {
    case note(String)
    case photo(URL)
    case poll(options: [String])

    enum Keys: String, CodingKey { case kind, text, url, options }

    init(from decoder: any Decoder) throws {
        let item = try decoder.container(keyedBy: Keys.self)
        switch try item.decode(String.self, forKey: .kind) {
        case "note":  self = .note(try item.decode(String.self, forKey: .text))
        case "photo": self = .photo(try item.decode(URL.self, forKey: .url))
        case "poll":  self = .poll(options: try item.decode([String].self, forKey: .options))
        case let other:
            throw DecodingError.dataCorruptedError(
                forKey: .kind, in: item, debugDescription: "No FeedItem kind named \(other)")
        }
    }
}

let feed = try JSONDecoder().decode([FeedItem].self, from: payload)
```

## Date decoding strategies

Set the strategy on the decoder to match what the server sends.

```swift
let reader = JSONDecoder()

reader.dateDecodingStrategy = .iso8601               // "2025-11-02T08:15:00Z"
reader.dateDecodingStrategy = .secondsSince1970      // 1762071300

let dayFormat = DateFormatter()
dayFormat.dateFormat = "dd.MM.yyyy"
dayFormat.locale = Locale(identifier: "en_US_POSIX")
dayFormat.timeZone = TimeZone(secondsFromGMT: 0)
reader.dateDecodingStrategy = .formatted(dayFormat)  // "02.11.2025"
```

When one field arrives in more than one shape, decode it by hand:

```swift
reader.dateDecodingStrategy = .custom { decoder in
    let raw = try decoder.singleValueContainer()
    let text = try raw.decode(String.self)
    let withFraction = ISO8601DateFormatter()
    withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    if let date = withFraction.date(from: text) ?? ISO8601DateFormatter().date(from: text) {
        return date
    }
    throw DecodingError.dataCorruptedError(in: raw, debugDescription: "Unreadable date: \(text)")
}
```

Give the encoder the matching strategy, for example
`writer.dateEncodingStrategy = .iso8601`, so values round-trip.

## Data and key strategies

```swift
reader.dataDecodingStrategy = .base64
writer.dataEncodingStrategy = .base64

reader.keyDecodingStrategy = .convertFromSnakeCase   // "first_name" -> firstName
writer.keyEncodingStrategy = .convertToSnakeCase
```

The snake-case strategies are mechanical. They split on underscores and
capitalize each following word, nothing more, so:

| JSON key | Property name produced |
| --- | --- |
| `avatar_url` | `avatarUrl` |
| `base_uri` | `baseUri` |
| `owner_id` | `ownerId` |

A model that spells those `avatarURL`, `baseURI` or `ownerID` needs explicit
`CodingKeys` for exactly those properties. The strategy still handles every
ordinary key.

## Lossy array decoding

By default a single bad element throws, and the whole array is lost. When a
feed is known to be unreliable, wrap each element so a failure becomes `nil`
and the container still moves on:

```swift
struct TolerantList<Element: Decodable>: Decodable {
    let items: [Element]

    private struct Slot: Decodable {
        let value: Element?
        init(from decoder: any Decoder) throws {
            value = try? Element(from: decoder)
        }
    }

    init(from decoder: any Decoder) throws {
        var list = try decoder.unkeyedContainer()
        var kept: [Element] = []
        while !list.isAtEnd {
            if let value = try list.decode(Slot.self).value {
                kept.append(value)
            }
        }
        items = kept
    }
}
```

Decoding `Slot` always succeeds, so the unkeyed container always advances.
A skip trick that decodes an empty placeholder struct only works when the bad
element is a JSON object; a bad string or number would throw again and leave
the loop stuck on the same index.

## Single value containers

A wrapper type that should appear in JSON as a bare value uses the single value
container in both directions.

```swift
struct OrderNumber: Codable, Hashable {
    let text: String

    init(from decoder: any Decoder) throws {
        text = try decoder.singleValueContainer().decode(String.self)
    }

    func encode(to encoder: any Encoder) throws {
        var slot = encoder.singleValueContainer()
        try slot.encode(text)
    }
}
// Encodes as "A-1042", not {"text":"A-1042"}.
```

## Default values for missing keys

A default on a stored property does not help synthesized decoding. With
`var language = "en"`, a payload without `language` still throws
`keyNotFound`. Either make the property optional or decode it yourself:

```swift
struct ReaderSettings: Codable {
    var language: String
    var textSize: Int
    var hyphenation: Bool

    init(from decoder: any Decoder) throws {
        let fields = try decoder.container(keyedBy: CodingKeys.self)
        language = try fields.decodeIfPresent(String.self, forKey: .language) ?? "en"
        textSize = try fields.decodeIfPresent(Int.self, forKey: .textSize) ?? 17
        hyphenation = try fields.decodeIfPresent(Bool.self, forKey: .hyphenation) ?? false
    }
}
```

`decodeIfPresent` returns `nil` for a key that is missing or `null`.

## Encoder and decoder configuration

```swift
let writer = JSONEncoder()
writer.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
```

`.sortedKeys` makes output stable, which is what snapshot and golden-file tests
need.

JSON has no NaN or infinity. If a `Double` can hold them, map them to strings
explicitly:

```swift
writer.nonConformingFloatEncodingStrategy = .convertToString(
    positiveInfinity: "inf", negativeInfinity: "-inf", nan: "nan")
reader.nonConformingFloatDecodingStrategy = .convertFromString(
    positiveInfinity: "inf", negativeInfinity: "-inf", nan: "nan")
```

### PropertyListEncoder and PropertyListDecoder

The same models work with property lists:

```swift
let plistWriter = PropertyListEncoder()
plistWriter.outputFormat = .binary        // or .xml when a human will read it
let stored = try plistWriter.encode(settings)
let restored = try PropertyListDecoder().decode(ReaderSettings.self, from: stored)
```

## Codable with URLSession

Check the HTTP status before decoding; a 404 body decoded as your model yields
a confusing error.

```swift
enum FetchFailure: Error { case badStatus(Int), notHTTP }

func fetchAuthors(from endpoint: URL) async throws -> [Author] {
    let (body, reply) = try await URLSession.shared.data(from: endpoint)
    guard let http = reply as? HTTPURLResponse else { throw FetchFailure.notHTTP }
    guard (200...299).contains(http.statusCode) else { throw FetchFailure.badStatus(http.statusCode) }

    let reader = JSONDecoder()
    reader.keyDecodingStrategy = .convertFromSnakeCase   // CodingKeys still cover URL, URI, ID names
    reader.dateDecodingStrategy = .iso8601
    return try reader.decode([Author].self, from: body)
}
```

### Generic response envelope

```swift
struct Envelope<Payload: Decodable>: Decodable {
    let data: Payload
    let paging: Paging?

    struct Paging: Decodable {
        let page: Int
        let pageCount: Int
    }
}

func unwrap<Payload: Decodable>(_ type: Payload.Type, from body: Data) throws -> Payload {
    let reader = JSONDecoder()
    reader.keyDecodingStrategy = .convertFromSnakeCase
    reader.dateDecodingStrategy = .iso8601
    return try reader.decode(Envelope<Payload>.self, from: body).data
}
```

The helper builds its own decoder. A decoder configured inside another function
is not in scope here, and code that refers to it will not compile.

## Codable with SwiftData

SwiftData stores the noncomputed stored properties of an `@Model` class when
their types are supported, and Codable structs and enums are supported. Use
them as typed properties directly:

```swift
struct PostalAddress: Codable {
    var street: String
    var city: String
    var postcode: String
}

@Model
final class Customer {
    var name: String
    var billing: PostalAddress?

    init(name: String, billing: PostalAddress? = nil) {
        self.name = name
        self.billing = billing
    }
}
```

Do not suggest `@Attribute(.transformable)`, a JSON `Data` blob or a JSON
`String` column as a workaround here. Keep the property typed. If a design
needs something SwiftData cannot store, hand it to the SwiftData skill.

## Codable with UserDefaults

`@AppStorage` is for small preferences: `Bool`, numbers, `String`, `URL`,
`Data`, or a `RawRepresentable` whose raw value is `Int` or `String`. A small
Codable preference can ride on a JSON string raw value:

```swift
struct PlaybackPrefs: Codable {
    var speed: Double = 1.0
    var skipSilence = false

    init() {}

    enum CodingKeys: String, CodingKey { case speed, skipSilence }

    init(from decoder: any Decoder) throws {
        let fields = try decoder.container(keyedBy: CodingKeys.self)
        speed = try fields.decodeIfPresent(Double.self, forKey: .speed) ?? 1.0
        skipSilence = try fields.decodeIfPresent(Bool.self, forKey: .skipSilence) ?? false
    }

    func encode(to encoder: any Encoder) throws {
        var fields = encoder.container(keyedBy: CodingKeys.self)
        try fields.encode(speed, forKey: .speed)
        try fields.encode(skipSilence, forKey: .skipSilence)
    }
}

extension PlaybackPrefs: RawRepresentable {
    init?(rawValue: String) {
        guard let bytes = rawValue.data(using: .utf8),
              let value = try? JSONDecoder().decode(PlaybackPrefs.self, from: bytes) else { return nil }
        self = value
    }

    var rawValue: String {
        guard let bytes = try? JSONEncoder().encode(self),
              let text = String(data: bytes, encoding: .utf8) else { return "{}" }
        return text
    }
}

struct PlaybackSettingsView: View {
    @AppStorage("playbackPrefs") private var prefs = PlaybackPrefs()

    var body: some View {
        Toggle("Skip silence", isOn: $prefs.skipSilence)
    }
}
```

Two details make this bridge safe:

- `encode(to:)` and `init(from:)` are written out. When a type is both
  `RawRepresentable` and Codable, the standard library's `RawRepresentable`
  defaults would otherwise encode `rawValue`, which encodes `self`, which
  recurses until the stack overflows.
- Decoding uses `decodeIfPresent` with defaults, so the `"{}"` fallback and
  values stored by an older version with fewer keys still decode. Synthesized
  decoding would ignore the property defaults and fail on `{}`.

## Common mistakes

1. **`decode` for a key that may be absent.** It throws `keyNotFound`. Use
   `decodeIfPresent` with `??` and a default.
2. **One bad element sinks the array.** Use a tolerant wrapper such as
   `TolerantList` or decode elements one by one.
3. **ISO dates with the default strategy.** The default is `.deferredToDate`,
   which expects a number of seconds since 1 January 2001. Set `.iso8601` for
   ISO 8601 strings.
4. **Force-unwrapping `try?` results.** Bind with `guard let` and handle the
   failure.
5. **Codable on read-only responses.** Declare `Decodable`.
6. **Hand-mapping every snake_case key.** Set `.convertFromSnakeCase` once and
   keep `CodingKeys` for the acronym names the strategy cannot produce.

## Review checklist

- [ ] `Decodable` alone where encoding is not needed
- [ ] Optional or missing keys use `decodeIfPresent` with defaults
- [ ] `.convertFromSnakeCase` for plain snake_case APIs; `CodingKeys` for URL, URI, ID style names
- [ ] Date strategy matches the API's format
- [ ] Unreliable arrays decode tolerantly
- [ ] Conversion and validation happen in `init(from:)`, not in fix-ups after decoding
- [ ] `.sortedKeys` where output is compared in tests
- [ ] Wrapper types use `singleValueContainer()`
- [ ] Responses share a generic envelope such as `Envelope<Payload>`
- [ ] No force unwraps of decoded values
- [ ] Persistence boundary is explicit: typed Codable properties on SwiftData models, `@AppStorage` only for small primitive or `RawRepresentable` preferences

## References

- [Codable](https://developer.apple.com/documentation/swift/codable)
- [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)
- [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder)
- [CodingKey](https://developer.apple.com/documentation/swift/codingkey)
- [JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase](https://developer.apple.com/documentation/foundation/jsondecoder/keydecodingstrategy/convertfromsnakecase)
- [Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/encoding-and-decoding-custom-types)
- [Using JSON with Custom Types](https://developer.apple.com/documentation/foundation/using-json-with-custom-types)
- [Preserving your app's model data across launches](https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches)
