iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Codable (JSON)

Codable is Swifts unified protocol for encoding and decoding values to and from external formats. Conform a type to Codable (or both Encodable + Decodable) and the compiler synthesises the JSON mapping. Customise with CodingKeys, custom init(from:), and the JSONDecoder configuration (date strategies, key strategies) to handle messy APIs.

Decode JSON, handle snake_case, dates, and unions

EXAMPLE
import Foundation

// 1) Basic Codable conformance — fields map by name
struct Money: Codable, Equatable {
    let amount: Int
    let currency: String
}

// 2) Snake_case from the API mapped to camelCase in Swift
struct Order: Codable {
    let id: String
    let customer: String
    let total: Money
    let createdAt: Date          // mapped from "created_at" by the decoder strategy
    let notes: String?           // optional => key may be missing or null
}

let json = """
[
  { "id": "o1", "customer": "alice",
    "total": {"amount": 4995, "currency": "AUD"},
    "created_at": "2026-06-11T09:30:00Z" },
  { "id": "o2", "customer": "bob",
    "total": {"amount": 0, "currency": "AUD"},
    "created_at": "2026-06-11T10:05:00Z", "notes": "free sample" }
]
""".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

let orders = try decoder.decode([Order].self, from: json)
print(orders)

// 3) Custom CodingKeys for when the names cannot be inferred
struct Profile: Codable {
    let displayName: String
    let avatarURL: URL
    private enum CodingKeys: String, CodingKey {
        case displayName = "display_name"
        case avatarURL   = "avatar_url"
    }
}

// 4) Sum types via a discriminator field
enum Event: Codable {
    case orderPaid(orderId: String)
    case orderShipped(orderId: String, tracking: String)

    private enum CodingKeys: String, CodingKey { case type, orderId, tracking }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        switch try c.decode(String.self, forKey: .type) {
        case "paid":
            self = .orderPaid(orderId: try c.decode(String.self, forKey: .orderId))
        case "shipped":
            self = .orderShipped(
                orderId: try c.decode(String.self, forKey: .orderId),
                tracking: try c.decode(String.self, forKey: .tracking))
        default:
            throw DecodingError.dataCorruptedError(forKey: .type, in: c,
                debugDescription: "Unknown event type")
        }
    }

    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .orderPaid(let id):
            try c.encode("paid", forKey: .type)
            try c.encode(id, forKey: .orderId)
        case .orderShipped(let id, let tracking):
            try c.encode("shipped", forKey: .type)
            try c.encode(id, forKey: .orderId)
            try c.encode(tracking, forKey: .tracking)
        }
    }
}

// 5) Encode back to JSON for a request body
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
let body = try encoder.encode(orders[0])
print(String(data: body, encoding: .utf8)!)

Why it matters

Set the JSONDecoders dateDecodingStrategy and keyDecodingStrategy once at the call site and your domain types stay clean — no per-property workaround for snake_case or ISO dates. The same pattern applies to JSONEncoder; configure both ends symmetrically so round-trips are lossless.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
struct User: Codable { let name: String; let age: Int }
let u = User(name: "Ada", age: 36)
let data = try JSONEncoder().encode(u)
print(String(data: data, encoding: .utf8)!)
let back = try JSONDecoder().decode(User.self, from: data)
Try it Yourself »

Exercise

Make a type encodable + decodable.

struct User: { let name: String }

Discussion

Loading…