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))
(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
.selfindecode(User.self, ...). - A property that isn't
Codable— "type 'X' does not conform to 'Decodable'". Make it Codable, exclude it fromCodingKeys, or write custom coding. - Using
Stringfor 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
Codabledoesn't handle subclasses well. Prefer structs.
Exercises¶
- Define a
Bookmodel with title, authors (array), publication date, and optional ISBN. Encode it to pretty-printed JSON and decode it back; assert equality (make itEquatable). - Decode this snake_case payload into camelCase properties with a key strategy:
{"user_id": 7, "created_at": "2026-01-02T03:04:05Z"}. - Write
CodingKeysto rename two properties without using a strategy. - Implement custom decoding for a
Colorthat accepts either"#ff8800"or{"r":255,"g":136,"b":0}. - Implement the
JSONValueenum above and write a function that pretty-prints any JSON string. - Persist an array of your tasks (beginner capstone!) as JSON instead of the pipe-delimited text file.
Recap¶
- Conform to
Codable; the compiler synthesizesinit(from:)andencode(to:). JSONEncoder/JSONDecoderconvert to/fromData; configure date and key strategies.- Rename with
CodingKeys, tolerate missing keys withdecodeIfPresent, and write custom coding for odd shapes. - Decoding errors carry a coding path — use it.
Next: Swift Package Manager →