iosNudge

How does Codable work? How do you handle keys and types that do not match your model?

EasyAsked oftenNetworkingSwift 1 min read

TL;DR

Codable is Encodable & Decodable. If all properties are Codable, the compiler writes the code for you. When JSON does not match your model, use (in this order): decoder strategies, CodingKeys, then a custom init(from:). Optional properties are fine when a key may be missing.

The basic case

struct Movie: Codable {
    let id: Int
    let title: String
    let rating: Double?
}

let json = #"{"id": 1, "title": "Dune", "rating": 8.1}"#.data(using: .utf8)!
let movie = try JSONDecoder().decode(Movie.self, from: json)

If rating is missing or null, it becomes nil. If title is missing, decoding throws.

Decoder strategies

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase   // release_date -> releaseDate
decoder.dateDecodingStrategy = .iso8601               // "2026-01-31T10:00:00Z"

These fix the whole payload with one line. Create the decoder once and reuse it.

CodingKeys for different names

struct Movie: Codable {
    let id: Int
    let title: String
    let posterURL: URL

    enum CodingKeys: String, CodingKey {
        case id
        case title = "original_title"
        case posterURL = "poster_path"
    }
}

Leave a property out of CodingKeys to skip it. It then needs a default value.

Custom init(from:)

Use it when types or structure do not match.

struct Product: Decodable {
    let id: String
    let price: Decimal
    let tags: [String]

    enum CodingKeys: String, CodingKey { case id, price, tags }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        // id can be a number or a string
        if let intID = try? c.decode(Int.self, forKey: .id) {
            id = String(intID)
        } else {
            id = try c.decode(String.self, forKey: .id)
        }
        // price comes as a string like "19.99"
        let priceText = try c.decode(String.self, forKey: .price)
        price = Decimal(string: priceText) ?? 0
        // tags may be missing
        tags = try c.decodeIfPresent([String].self, forKey: .tags) ?? []
    }
}

Nested JSON

// {"data": {"user": {"name": "Ana"}}}
struct Envelope<T: Decodable>: Decodable { let data: T }
struct UserBox: Decodable { let user: User }

let name = try decoder.decode(Envelope<UserBox>.self, from: json).data.user.name

A generic wrapper is often simpler than nestedContainer.

Enums

enum Status: String, Codable { case active, banned }

// Swift 5.5+: associated values are synthesized too
enum Shape: Codable {
    case circle(radius: Double)
    case square(side: Double)
}

An unknown raw value like "pending" makes decoding throw. A safe pattern:

enum Status: String, Decodable {
    case active, banned, unknown
    init(from decoder: Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self = Status(rawValue: raw) ?? .unknown
    }
}

Reading errors

do {
    _ = try decoder.decode(Movie.self, from: data)
} catch let DecodingError.keyNotFound(key, context) {
    print("Missing", key.stringValue, "at", context.codingPath)
} catch let DecodingError.typeMismatch(type, context) {
    print("Wrong type", type, context.debugDescription)
}

codingPath tells you exactly where the problem is.

Common follow-up questions

  • One bad item breaks the whole array. How do you fix it? Decode each element as optional with a wrapper and drop the failures (a "lossy array").
  • Where should Codable live: in the domain model or a separate DTO? For messy APIs, use a DTO and map it to a clean model.
  • Is JSONDecoder thread-safe? Using one instance from many threads is fine in practice, but do not change its settings while it decodes. In Swift 6, JSONDecoder is Sendable.
  • How do you encode a Date in a custom format? Set dateEncodingStrategy = .formatted(formatter) or .custom { ... }.
  • Is Codable slow? It is fine for normal APIs. For huge payloads, decode on a background task.