Skip to content

9. Codable & JSON

What & why

Almost every real program converts its data to and from some external format: JSON from a web API, a config file, a saved document. Swift's Codable makes this nearly free: declare that a type is Codable and the compiler writes the encoding and decoding code for you.

Codable is just a typealias for Encodable & Decodable. JSONEncoder and JSONDecoder live in Foundation (available on all platforms).

Encoding to JSON

import Foundation

struct User: Codable {
    var id: Int
    var name: String
    var email: String?
    var tags: [String]
}

let user = User(id: 1, name: "Ada", email: nil, tags: ["math", "swift"])

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(user)                 // Data
print(String(decoding: data, as: UTF8.self))
{
  "id" : 1,
  "name" : "Ada",
  "tags" : [
    "math",
    "swift"
  ]
}

(The nil email is simply omitted.)

Decoding from JSON

let json = """
{ "id": 2, "name": "Grace", "email": "g@navy.mil", "tags": [] }
"""

let decoded = try JSONDecoder().decode(User.self, from: Data(json.utf8))
print(decoded.name)         // Grace

decode(User.self, from:) takes the type to decode (hence .self). If the JSON doesn't match — a missing non-optional key, a wrong type — it throws a DecodingError.

Handling decoding errors

do {
    _ = try JSONDecoder().decode(User.self, from: Data(#"{ "id": "oops" }"#.utf8))
} catch let DecodingError.typeMismatch(type, context) {
    print("Expected \(type) at \(context.codingPath.map(\.stringValue).joined(separator: "."))")
} catch let DecodingError.keyNotFound(key, _) {
    print("Missing key: \(key.stringValue)")
} catch {
    print(error)
}

DecodingError cases (keyNotFound, valueNotFound, typeMismatch, dataCorrupted) carry a coding path — exactly where in the document things went wrong. Surface that to users or logs.

What can be Codable?

A type is automatically Codable when all its stored properties are. That includes String, Int, Double, Bool, Date, URL, Data, UUID, Optional, Array, Dictionary (with String/Int keys), Set, and your own Codable structs, classes, and enums.

struct Order: Codable {
    var id: UUID
    var placedAt: Date
    var items: [Item]
    var status: Status

    struct Item: Codable { var sku: String; var quantity: Int }
    enum Status: String, Codable { case pending, shipped, delivered }
}

Enums with raw values encode as their raw value. Enums without raw values are Codable too (Swift 5.5+): cases with associated values encode as nested objects keyed by case name.

Customizing keys: CodingKeys

APIs often use snake_case while Swift uses camelCase. Easiest fix — a strategy:

struct Profile: Codable {
    var firstName: String
    var lastLogin: Date
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase     // first_name -> firstName
decoder.dateDecodingStrategy = .iso8601                  // "2026-10-16T12:00:00Z"

let p = try decoder.decode(Profile.self, from: Data(#"""
{"first_name": "Ada", "last_login": "2026-10-16T12:00:00Z"}
"""#.utf8))

(Encoders have the mirror: .convertToSnakeCase, .iso8601.)

For arbitrary renames, define CodingKeys — an enum whose cases are your properties, with raw values for the JSON names:

struct Repo: Codable {
    var name: String
    var stars: Int
    var isPrivate: Bool

    enum CodingKeys: String, CodingKey {
        case name
        case stars = "stargazers_count"
        case isPrivate = "private"
    }
}

Omit a case from CodingKeys to exclude a property from encoding/decoding (it must then have a default value).

Dates and other strategies

decoder.dateDecodingStrategy = .iso8601
decoder.dateDecodingStrategy = .secondsSince1970
decoder.dateDecodingStrategy = .formatted(someDateFormatter)
decoder.dataDecodingStrategy = .base64          // the default
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "inf", negativeInfinity: "-inf", nan: "nan")

The default for Date is a number of seconds since a 2001 reference date — rarely what a web API uses, so always set a strategy.

Custom init(from:) and encode(to:)

When the JSON shape differs from your model, write the code yourself:

struct Point: Codable {
    var x: Double
    var y: Double

    init(x: Double, y: Double) { self.x = x; self.y = y }

    // JSON is [x, y] rather than {"x":…, "y":…}
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        x = try container.decode(Double.self)
        y = try container.decode(Double.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(x)
        try container.encode(y)
    }
}

let point = try JSONDecoder().decode(Point.self, from: Data("[3, 4]".utf8))

A keyed example that supplies defaults and tolerates a missing key:

struct Settings: Decodable {
    var theme: String
    var fontSize: Int

    enum CodingKeys: String, CodingKey { case theme, fontSize }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        theme = try c.decodeIfPresent(String.self, forKey: .theme) ?? "light"
        fontSize = try c.decodeIfPresent(Int.self, forKey: .fontSize) ?? 14
    }
}

decodeIfPresent returns nil when the key is absent or null.

Nested and heterogeneous JSON

Flattening a nested structure:

struct Weather: Decodable {
    var city: String
    var temperature: Double

    enum CodingKeys: String, CodingKey { case city = "name", main }
    enum MainKeys: String, CodingKey { case temp }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        city = try c.decode(String.self, forKey: .city)
        let main = try c.nestedContainer(keyedBy: MainKeys.self, forKey: .main)
        temperature = try main.decode(Double.self, forKey: .temp)
    }
}
// JSON: {"name":"Oslo","main":{"temp":3.5}}

For JSON whose shape you don't know in advance, define a recursive enum:

enum JSONValue: Codable, Equatable {
    case null, bool(Bool), number(Double), string(String)
    case array([JSONValue]), object([String: JSONValue])

    init(from decoder: Decoder) throws {
        let c = try decoder.singleValueContainer()
        if c.decodeNil() { self = .null }
        else if let v = try? c.decode(Bool.self) { self = .bool(v) }
        else if let v = try? c.decode(Double.self) { self = .number(v) }
        else if let v = try? c.decode(String.self) { self = .string(v) }
        else if let v = try? c.decode([JSONValue].self) { self = .array(v) }
        else { self = .object(try c.decode([String: JSONValue].self)) }
    }

    func encode(to encoder: Encoder) throws {
        var c = encoder.singleValueContainer()
        switch self {
        case .null: try c.encodeNil()
        case .bool(let v): try c.encode(v)
        case .number(let v): try c.encode(v)
        case .string(let v): try c.encode(v)
        case .array(let v): try c.encode(v)
        case .object(let v): try c.encode(v)
        }
    }
}

Other formats

Codable is format-agnostic. PropertyListEncoder/Decoder come with Foundation; third-party packages provide YAML, TOML, CBOR, MessagePack, and CSV coders that reuse your Codable types.

Files round trip

let url = URL(fileURLWithPath: "users.json")
try encoder.encode([user]).write(to: url, options: .atomic)
let users = try JSONDecoder().decode([User].self, from: Data(contentsOf: url))

Common mistakes

  • Forgetting .self in decode(User.self, ...).
  • A property that isn't Codable — "type 'X' does not conform to 'Decodable'". Make it Codable, exclude it from CodingKeys, or write custom coding.
  • Using String for everything from a loosely-typed API, then crashing later. Decode to proper types.
  • Not configuring the date strategy and getting garbage or a type mismatch.
  • Making every property optional "just in case" — model what's truly optional, and let decoding fail loudly for the rest.
  • Ignoring the coding path in errors when debugging.
  • Using classes with inheritance — synthesized Codable doesn't handle subclasses well. Prefer structs.

Exercises

  1. Define a Book model with title, authors (array), publication date, and optional ISBN. Encode it to pretty-printed JSON and decode it back; assert equality (make it Equatable).
  2. Decode this snake_case payload into camelCase properties with a key strategy: {"user_id": 7, "created_at": "2026-01-02T03:04:05Z"}.
  3. Write CodingKeys to rename two properties without using a strategy.
  4. Implement custom decoding for a Color that accepts either "#ff8800" or {"r":255,"g":136,"b":0}.
  5. Implement the JSONValue enum above and write a function that pretty-prints any JSON string.
  6. Persist an array of your tasks (beginner capstone!) as JSON instead of the pipe-delimited text file.

Recap

  • Conform to Codable; the compiler synthesizes init(from:) and encode(to:).
  • JSONEncoder/JSONDecoder convert to/from Data; configure date and key strategies.
  • Rename with CodingKeys, tolerate missing keys with decodeIfPresent, and write custom coding for odd shapes.
  • Decoding errors carry a coding path — use it.

Next: Swift Package Manager →