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
- Basic Conformance
- Custom CodingKeys
- Custom Decoding and Encoding
- Nested and Flattened Containers
- Heterogeneous Arrays
- Date Decoding Strategies
- Data and Key Strategies
- Lossy Array Decoding
- Single Value Containers
- Default Values for Missing Keys
- Encoder and Decoder Configuration
- Codable with URLSession
- Codable with SwiftData
- Codable with UserDefaults
- Common Mistakes
- Review Checklist
- References
Decode and Verify Workflow
- Decode representative success, missing, null, malformed, acronym-key, and date fixtures.
- On failure, inspect
DecodingError, itscodingPath, 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:
swiftstruct 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:
swiftstruct 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:
swiftstruct 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) // Decode Unix timestamp as Double, convert to Date let epoch = try container.decode(Double.self, forKey: .timestamp) timestamp = Date(timeIntervalSince1970: epoch) // Default to empty array when key is missing tags = try container.decodeIfPresent([String].self, forKey: .tags) ?? [] } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(timestamp.timeIntervalSince1970, forKey: .timestamp) try container.encode(tags, forKey: .tags) } }
Nested and Flattened Containers
Use nestedContainer(keyedBy:forKey:) to navigate and flatten nested JSON:
swift// JSON: { "id": 1, "location": { "lat": 37.7749, "lng": -122.4194 } } 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:
swiftlet decoder = JSONDecoder() // ISO 8601 (e.g., "2024-03-15T10:30:00Z") decoder.dateDecodingStrategy = .iso8601 // Unix timestamp in seconds (e.g., 1710499800) decoder.dateDecodingStrategy = .secondsSince1970 // Custom DateFormatter let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(secondsFromGMT: 0) decoder.dateDecodingStrategy = .formatted(formatter) // Custom closure for multiple formats 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
swiftlet decoder = JSONDecoder() decoder.dataDecodingStrategy = .base64 // Base64-encoded Data fields decoder.keyDecodingStrategy = .convertFromSnakeCase // simple keys only; not URL/ID spelling // {"user_name": "Alice"} maps to `var userName: String` -- no CodingKeys needed 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
swiftfunc 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) } // Generic API envelope. Configure a decoder inside this helper because // fetchUser's decoder is out of scope. struct APIResponse<T: Decodable>: Decodable { let data: T let meta: Meta? struct Meta: Decodable { let page: Int; let totalPages: Int } } func decodeUsersEnvelope(from data: Data) throws -> [User] { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .iso8601 return try decoder.decode(APIResponse<[User]>.self, 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:
swift// DON'T -- crashes if key is absent let value = try container.decode(String.self, forKey: .bio) // DO -- falls back when the key is absent or null let value = try container.decodeIfPresent(String.self, forKey: .bio) ?? ""
2. Failing entire array when one element is invalid:
swift// DON'T -- one bad element kills the whole decode let items = try container.decode([Item].self, forKey: .items) // DO -- decode elements individually only when partial success is allowed
3. Date strategy mismatch:
swift// DON'T -- default strategy expects Double, but API sends ISO string let decoder = JSONDecoder() // dateDecodingStrategy defaults to .deferredToDate // DO -- set strategy to match your API format decoder.dateDecodingStrategy = .iso8601
4. Force-unwrapping decoded optionals:
swift// DON'T let user = try? decoder.decode(User.self, from: data) print(user!.name) // DO guard let user = try? decoder.decode(User.self, from: data) else { return }
5. Using Codable when only Decodable is needed:
swift// DON'T -- unnecessarily constrains the type to also be Encodable struct APIResponse: Codable { let id: Int; let message: String } // DO -- use Decodable for read-only API responses struct APIResponse: Decodable { let id: Int; let message: String }
6. Manual CodingKeys for simple snake_case APIs:
swift// DON'T -- verbose boilerplate for every model enum CodingKeys: String, CodingKey { case userName = "user_name" case avatarUrl = "avatar_url" } // DO -- configure once on the decoder for simple cases decoder.keyDecodingStrategy = .convertFromSnakeCase // Keep CodingKeys for `imageURL`, `baseURI`, `userID`, and similar names.
Review Checklist
- Types conform to
Decodableonly when encoding is not needed -
decodeIfPresentused with defaults for optional or missing keys -
keyDecodingStrategy = .convertFromSnakeCaseused for simple snake_case APIs, with CodingKeys retained for acronym spellings -
dateDecodingStrategymatches the API date format - Arrays of unreliable data use lossy decoding to skip invalid elements
- Custom
init(from:)validates and transforms data instead of post-decode fixups -
JSONEncoder.outputFormattingincludes.sortedKeysfor deterministic test output - Wrapper types (UserID, etc.) use
singleValueContainerfor clean JSON - Generic
APIResponse<T>wrapper used for consistent API envelope handling - No force-unwrapping of decoded values
- Persistence boundary is explicit: SwiftData only for compatible noncomputed model properties,
@AppStorage/UserDefaults only for small primitive orRawRepresentablepreferences
References
- Advanced Codable patterns -- mixed arrays, lossy decoding, wrappers, defaults, configuration, and persistence boundaries
- Codable -- protocol combining Encodable and Decodable
- JSONDecoder -- decodes JSON data into Codable types
- JSONEncoder -- encodes Codable types as JSON data
- CodingKey -- protocol for encoding/decoding keys
- JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase -- snake-case conversion behavior and limitations
- Encoding and Decoding Custom Types -- Apple guide on custom Codable conformance
- Using JSON with Custom Types -- Apple sample code for JSON patterns
- Preserving your app's model data across launches -- SwiftData model property compatibility


