Skip to content
Codable: JSON Encoding and Decoding
step 1/6

Reading — step 1 of 6

Learn

~2 min readConcurrency

Codable (Swift 4+) is the standard way to encode/decode types to/from data formats — primarily JSON. It replaces hand-written serialization with an automatic, type-safe approach.

The basics

import Foundation

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

// Decode JSON to User
let json = #"{"id":42,"name":"Alice","email":"[email protected]"}"#
let data = json.data(using: .utf8)!
let user = try JSONDecoder().decode(User.self, from: data)
print(user.name)   // Alice

// Encode User to JSON
let encoded = try JSONEncoder().encode(user)
let jsonString = String(data: encoded, encoding: .utf8)!
print(jsonString)

Codable is Encodable & Decodable. If all your stored properties are Codable (which is true for common types), you get conformance for free.

CodingKeys for renaming

Most APIs use snake_case JSON; Swift uses camelCase. Map them with CodingKeys:

struct User: Codable {
    let id: Int
    let firstName: String
    let lastName: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case firstName = "first_name"
        case lastName = "last_name"
    }
}

Or use the encoder's keyEncodingStrategy for batch conversion:

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

Date and Data formatting

Dates and binary data need explicit format choices:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
// or .millisecondsSince1970, .secondsSince1970, .formatted(formatter), .custom(...)

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

Optional fields

Missing JSON fields decode to nil for Optional properties:

struct User: Codable {
    let name: String
    let email: String?     // nil if missing in JSON
}

Nested types

Nested Codables work transparently:

struct Order: Codable {
    let id: Int
    let user: User
    let items: [Item]
}

struct Item: Codable {
    let name: String
    let price: Double
}

The whole tree decodes/encodes recursively.

Custom decoding for tricky JSON

For unusual shapes, implement init(from:):

struct Distance: Codable {
    let meters: Double
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        // API returns the number as a string for some reason:
        let str = try container.decode(String.self)
        guard let value = Double(str) else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "not a number")
        }
        self.meters = value
    }
}

Handling errors

do {
    let user = try JSONDecoder().decode(User.self, from: data)
} catch DecodingError.keyNotFound(let key, _) {
    print("missing key: \(key.stringValue)")
} catch DecodingError.typeMismatch(let type, let context) {
    print("type mismatch: expected \(type), \(context.debugDescription)")
} catch {
    print("other: \(error)")
}

DecodingError cases tell you exactly what went wrong — useful for surfacing API bugs.

Common mistakes

  • Forgetting Codable on nested types — error "Type does not conform." Mark the entire tree.
  • Mismatched JSON key cases — use CodingKeys or keyDecodingStrategy.
  • Decoding Date without specifying strategy — defaults are sometimes wrong. Be explicit.
  • Holding the decoded result by reference when it's a struct — structs are copied. Mutate or pass around as needed.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…