| name | swift-codable |
| description | Implement Swift Codable models for JSON and property-list encoding and decoding with JSONDecoder, JSONEncoder, CodingKeys, and custom init(from:) or encode(to:). Use when parsing API responses, remapping keys, flattening nested JSON, handling date or data decoding strategies, decoding heterogeneous arrays, or integrating Codable with URLSession, SwiftData, or UserDefaults. |
Swift Codable
Encode and decode Swift types using Codable (Encodable & Decodable) with
JSONEncoder, JSONDecoder, and related APIs. Targets Swift 6.3 / iOS 26+.
Contents
Decode and Verify Workflow
- Decode representative success, missing, null, malformed, acronym-key, and
date fixtures.
- On failure, inspect
DecodingError, its codingPath, and the raw payload.
- Correct only the mismatched model, key, container, or strategy; do not hide
contract failures with lossy decoding.
- Rerun fixtures and encode/decode round trips where both directions are part
of the contract.
Basic Conformance
When all stored properties are themselves Codable, the compiler synthesizes
conformance automatically:
struct User: Codable {
let id: Int
let name: String
let email: String
let isVerified: Bool
}
let user = try JSONDecoder().decode(User.self, from: jsonData)
let encoded = try JSONEncoder().encode(user)
Prefer Decodable for read-only API responses and Encodable for write-only.
Use Codable only when both directions are required.
Custom CodingKeys
Rename JSON keys without writing a custom decoder by declaring a CodingKeys
enum:
struct Product: Codable {
let id: Int
let displayName: String
let imageURL: URL
let priceInCents: Int
enum CodingKeys: String, CodingKey {
case id
case displayName = "display_name"
case imageURL = "image_url"
case priceInCents = "price_in_cents"
}
}
Every stored property must appear in the enum. Omitting a property from
CodingKeys excludes it from encoding/decoding -- provide a default value or
compute it separately.
Custom Decoding and Encoding
Override init(from:) and encode(to:) for transformations the synthesized
conformance cannot handle:
struct Event: Codable {
let name: String
let timestamp: Date
let tags: [String]
enum CodingKeys: String, CodingKey {
case name, timestamp, tags
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
let epoch = try container.decode(Double.self, forKey: .timestamp)
timestamp = Date(timeIntervalSince1970: epoch)
tags = try container.decodeIfPresent([String].self, forKey: .tags) ?? []
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: .)
container.encode(name, forKey: .name)
container.encode(timestamp.timeIntervalSince1970, forKey: .timestamp)
container.encode(tags, forKey: .tags)
}
}
Nested and Flattened Containers
Use nestedContainer(keyedBy:forKey:) to navigate and flatten nested JSON:
struct Place: Decodable {
let id: Int
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey { case id, location }
enum LocationKeys: String, CodingKey { case lat, lng }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let location = try container.nestedContainer(
keyedBy: LocationKeys.self, forKey: .location)
latitude = try location.decode(Double.self, forKey: .lat)
longitude = try location.decode(Double.self, forKey: .lng)
}
}
Chain multiple nestedContainer calls to flatten deeply nested structures.
Also use nestedUnkeyedContainer(forKey:) for nested arrays.
Heterogeneous Arrays
Load Advanced Codable Patterns
for discriminator-based mixed arrays.
Date Decoding Strategies
Configure JSONDecoder.dateDecodingStrategy to match your API:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.dateDecodingStrategy = .secondsSince1970
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(formatter)
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
if let date = ISO8601DateFormatter().date(from: string) { return date }
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Cannot decode date: \(string)")
}
Set the matching strategy on JSONEncoder:
encoder.dateEncodingStrategy = .iso8601
Data and Key Strategies
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
decoder.keyDecodingStrategy = .convertFromSnakeCase
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.keyEncodingStrategy = .convertToSnakeCase
Use key strategies only for mechanical snake_case-to-camelCase mappings.
convertFromSnakeCase maps by spelling, not Swift acronym/initialism policy:
image_url, base_uri, and user_id match imageUrl, baseUri, and
userId only. If the Swift model uses imageURL, baseURI, or userID,
declare explicit CodingKeys; the strategy will not synthesize those names.
Lossy Array Decoding
Use lossy arrays only when partial success is part of the product contract; load
Lossy Arrays.
Single Value Containers
Use singleValueContainer() for type-safe primitive wrappers; see
Single-Value Wrappers.
Default Values for Missing Keys
Stored defaults do not make synthesized decoding tolerate missing nonoptional
keys. Load Missing-Key Defaults
when the contract assigns explicit fallback behavior to missing or null values.
Encoder and Decoder Configuration
Keep matching strategies at the transport/file-format boundary. Load
Encoder Configuration
for nonconforming floats and property-list guidance.
Codable with URLSession
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw APIError.invalidResponse
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(User.self, from: data)
}
struct APIResponse<T: Decodable>: Decodable {
let data: T
let meta: Meta?
struct Meta: { page: ; totalPages: }
}
( : ) -> [] {
decoder ()
decoder.keyDecodingStrategy .convertFromSnakeCase
decoder.dateDecodingStrategy .iso8601
decoder.decode(<[]>., from: data).data
}
Codable with SwiftData
Keep schema values typed and route persistence design to swiftdata; see
Persistence Boundaries.
Codable with UserDefaults
Use primitives for small preferences. Load
Persistence Boundaries
for a small Codable RawRepresentable/@AppStorage handoff; use a real
persistence layer for larger or durable data.
Common Mistakes
1. Not handling missing defaulted fields:
let value = try container.decode(String.self, forKey: .bio)
let value = try container.decodeIfPresent(String.self, forKey: .bio) ?? ""
2. Failing entire array when one element is invalid:
let items = try container.decode([Item].self, forKey: .items)
3. Date strategy mismatch:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
4. Force-unwrapping decoded optionals:
let user = try? decoder.decode(User.self, from: data)
print(user!.name)
guard let user = try? decoder.decode(User.self, from: data) else { return }
5. Using Codable when only Decodable is needed:
struct APIResponse: Codable { let id: Int; let message: String }
struct APIResponse: Decodable { let id: Int; let message: String }
6. Manual CodingKeys for simple snake_case APIs:
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case avatarUrl = "avatar_url"
}
decoder.keyDecodingStrategy = .convertFromSnakeCase
Review Checklist
References