--- name: axiom-codable description: Use when working with Codable protocol, JSON encoding/decoding, CodingKeys customization, enum serialization, date strategies, custom containers, or encountering "Type does not conform to Decodable/Encodable" errors - comprehensive Codable patterns and anti-patterns for Swift 6.x license: MIT metadata: version: "1.0" --- # Swift Codable Patterns Comprehensive guide to Codable protocol conformance for JSON and PropertyList encoding/decoding in Swift 6.x. ## Quick Reference ### Decision Tree: When to Use Each Approach ``` Has your type... ├─ All properties Codable? → Automatic synthesis (just add `: Codable`) ├─ Property names differ from JSON keys? → CodingKeys customization ├─ Needs to exclude properties? → CodingKeys customization ├─ Enum with associated values? → Check enum synthesis patterns ├─ Needs structural transformation? → Manual implementation + bridge types ├─ Needs data not in JSON? → DecodableWithConfiguration (iOS 15+) └─ Complex nested JSON? → Manual implementation + nested containers ``` ### Common Triggers | Error | Solution | |-------|----------| | "Type 'X' does not conform to protocol 'Decodable'" | Ensure all stored properties are Codable | | "No value associated with key X" | Check CodingKeys match JSON keys | | "Expected to decode X but found Y instead" | Type mismatch; check JSON structure or use bridge type | | "keyNotFound" | JSON missing expected key; make property optional or provide default | | "Date parsing failed" | Configure dateDecodingStrategy on decoder | --- ## Part 1: Automatic Synthesis Swift automatically synthesizes Codable conformance when all stored properties are Codable. ### Struct Synthesis ```swift // ✅ Automatic synthesis struct User: Codable { let id: UUID // Codable var name: String // Codable var membershipPoints: Int // Codable } // JSON: {"id":"...", "name":"Alice", "membershipPoints":100} ``` **Requirements**: - All stored properties must conform to Codable - Properties use standard Swift types or other Codable types - No custom initialization logic needed ### Enum Synthesis Patterns #### Pattern 1: Raw Value Enums ```swift enum Direction: String, Codable { case north, south, east, west } // Encodes as: "north" ``` The raw value itself becomes the JSON representation. #### Pattern 2: Enums Without Associated Values ```swift enum Status: Codable { case success case failure case pending } // Encodes as: {"success":{}} ``` Each case becomes an object with the case name as the key and empty dictionary as value. #### Pattern 3: Enums With Associated Values ```swift enum APIResult: Codable { case success(data: String, count: Int) case error(code: Int, message: String) } // success case encodes as: // {"success":{"data":"example","count":5}} ``` **Gotcha**: Unlabeled associated values generate `_0`, `_1` keys: ```swift enum Command: Codable { case store(String, Int) // ❌ Unlabeled } // Encodes as: {"store":{"_0":"value","_1":42}} ``` **Fix**: Always label associated values for predictable JSON: ```swift enum Command: Codable { case store(key: String, value: Int) // ✅ Labeled } // Encodes as: {"store":{"key":"value","value":42}} ``` ### When Synthesis Breaks Automatic synthesis fails when: 1. **Computed properties** - Only stored properties are encoded 2. **Non-Codable properties** - Custom types without Codable conformance 3. **Property wrappers** - `@Published`, `@State` (except `@AppStorage` with Codable types) 4. **Class inheritance** - Subclasses must implement `init(from:)` manually --- ## Part 2: CodingKeys Customization Use `CodingKeys` enum to customize encoding/decoding without full manual implementation. ### Renaming Keys ```swift struct Article: Codable { let url: URL let title: String let body: String enum CodingKeys: String, CodingKey { case url = "source_link" // JSON uses "source_link" case title = "content_name" // JSON uses "content_name" case body // Matches JSON key } } // JSON: {"source_link":"...", "content_name":"...", "body":"..."} ``` ### Excluding Properties Omit properties from `CodingKeys` to exclude them from encoding/decoding: ```swift struct NoteCollection: Codable { let name: String let notes: [Note] var localDrafts: [Note] = [] // ✅ Must have default value enum CodingKeys: CodingKey { case name case notes // localDrafts omitted - not encoded/decoded } } ``` **Rule**: Excluded properties require default values or you must implement `init(from:)` manually. ### Snake Case Conversion For consistent snake_case → camelCase conversion: ```swift let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase // JSON: {"first_name":"Alice", "last_name":"Smith"} // Decodes to: User(firstName: "Alice", lastName: "Smith") ``` ### Enum Associated Value Keys Customize keys for enum associated values using `{CaseName}CodingKeys`: ```swift enum Command: Codable { case store(key: String, value: Int) case delete(key: String) enum StoreCodingKeys: String, CodingKey { case key = "identifier" // Renames "key" to "identifier" case value = "data" // Renames "value" to "data" } enum DeleteCodingKeys: String, CodingKey { case key = "identifier" } } // store case encodes as: {"store":{"identifier":"x","data":42}} ``` **Pattern**: `{CaseName}CodingKeys` with capitalized case name. --- ## Part 3: Manual Implementation For structural differences between JSON and Swift models, implement `init(from:)` and `encode(to:)`. ### Container Types | Container | When to Use | |-----------|-------------| | **Keyed** | Dictionary-like data with string keys | | **Unkeyed** | Array-like sequential data | | **Single-value** | Wrapper types that encode as a single value | | **Nested** | Hierarchical JSON structures | ### Nested Containers Example Flatten hierarchical JSON: ```swift // JSON: // { // "latitude": 37.7749, // "longitude": -122.4194, // "additionalInfo": { // "elevation": 52 // } // } struct Coordinate { var latitude: Double var longitude: Double var elevation: Double // Nested in JSON, flat in Swift enum CodingKeys: String, CodingKey { case latitude, longitude, additionalInfo } enum AdditionalInfoKeys: String, CodingKey { case elevation } } extension Coordinate: Decodable { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) latitude = try values.decode(Double.self, forKey: .latitude) longitude = try values.decode(Double.self, forKey: .longitude) let additionalInfo = try values.nestedContainer( keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo ) elevation = try additionalInfo.decode(Double.self, forKey: .elevation) } } extension Coordinate: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(latitude, forKey: .latitude) try container.encode(longitude, forKey: .longitude) var additionalInfo = container.nestedContainer( keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo ) try additionalInfo.encode(elevation, forKey: .elevation) } } ``` ### Bridge Types for Structural Mismatches When JSON structure fundamentally differs from Swift model: ```swift // JSON: {"USD": 1.0, "EUR": 0.85, "GBP": 0.73} // Want: [ExchangeRate] struct ExchangeRate { let currency: String let rate: Double } // Bridge type for decoding private extension ExchangeRate { struct List: Decodable { let values: [ExchangeRate] init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let dictionary = try container.decode([String: Double].self) values = dictionary.map { ExchangeRate(currency: $0, rate: $1) } } } } // Public interface extension ExchangeRate { static func decode(from data: Data) throws -> [ExchangeRate] { let list = try JSONDecoder().decode(List.self, from: data) return list.values } } ``` --- ## Part 4: Date Handling ### Built-in Strategies ```swift let decoder = JSONDecoder() // 1. ISO 8601 (recommended) decoder.dateDecodingStrategy = .iso8601 // Expects: "2024-02-15T17:00:00+01:00" // 2. Unix timestamp (seconds) decoder.dateDecodingStrategy = .secondsSince1970 // Expects: 1708012800 // 3. Unix timestamp (milliseconds) decoder.dateDecodingStrategy = .millisecondsSince1970 // Expects: 1708012800000 // 4. Custom formatter let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" formatter.locale = Locale(identifier: "en_US_POSIX") // ✅ Always set formatter.timeZone = TimeZone(secondsFromGMT: 0) // ✅ Always set decoder.dateDecodingStrategy = .formatted(formatter) // 5. Custom closure decoder.dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) if let date = ISO8601DateFormatter().date(from: dateString) { return date } throw DecodingError.dataCorruptedError( in: container, debugDescription: "Cannot decode date string \(dateString)" ) } ``` ### ISO 8601 Nuances **Default**: `2024-02-15T17:00:00+01:00` **Timezone required**: Without timezone offset, decoding may fail across regions ```swift // ❌ No timezone - parsing depends on device locale "2024-02-15T17:00:00" // ✅ With timezone - unambiguous "2024-02-15T17:00:00+01:00" ``` ### Performance Consideration **Custom closures run for every date** - optimize expensive operations: ```swift // ❌ Creates new formatter for every date decoder.dateDecodingStrategy = .custom { decoder in let formatter = DateFormatter() // Expensive! // ... } // ✅ Reuse formatter let sharedFormatter = DateFormatter() sharedFormatter.dateFormat = "yyyy-MM-dd" decoder.dateDecodingStrategy = .custom { decoder in // Use sharedFormatter } ``` --- ## Part 5: Type Transformation ### StringBacked Wrapper Handle APIs that encode numbers as strings: ```swift protocol StringRepresentable: CustomStringConvertible { init?(_ string: String) } extension Int: StringRepresentable {} extension Double: StringRepresentable {} struct StringBacked: Codable { var value: Value init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) guard let value = Value(string) else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Cannot convert '\(string)' to \(Value.self)" ) } self.value = value } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(value.description) } } // Usage struct Product: Codable { let name: String private let _price: StringBacked var price: Double { get { _price.value } set { _price = StringBacked(value: newValue) } } enum CodingKeys: String, CodingKey { case name case _price = "price" } } // JSON: {"name":"Widget","price":"19.99"} // Decodes to: Product(name: "Widget", price: 19.99) ``` ### Type Coercion For loosely typed APIs that may return different types: ```swift struct FlexibleValue: Codable { let stringValue: String init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let string = try? container.decode(String.self) { stringValue = string } else if let int = try? container.decode(Int.self) { stringValue = String(int) } else if let double = try? container.decode(Double.self) { stringValue = String(double) } else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Cannot decode value to String, Int, or Double" ) } } } ``` **Warning**: Avoid this pattern unless the API is truly unpredictable. Prefer strict types. --- ## Part 6: Advanced Patterns ### DecodableWithConfiguration (iOS 15+) For types that need data unavailable in JSON: ```swift struct User: Encodable, DecodableWithConfiguration { let id: UUID var name: String var favorites: Favorites // Not in JSON, injected via configuration enum CodingKeys: CodingKey { case id, name } init(from decoder: Decoder, configuration: Favorites) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) name = try container.decode(String.self, forKey: .name) favorites = configuration // Injected } } // Usage (iOS 17+) let favorites = try await fetchFavorites() let user = try JSONDecoder().decode( User.self, from: data, configuration: favorites ) ``` ### userInfo Workaround (iOS 15-16) ```swift extension JSONDecoder { private struct ConfigurationDecodingWrapper: Decodable { var wrapped: T init(from decoder: Decoder) throws { let config = decoder.userInfo[configurationUserInfoKey] as! T.DecodingConfiguration wrapped = try T(from: decoder, configuration: config) } } func decode( _ type: T.Type, from data: Data, configuration: T.DecodingConfiguration ) throws -> T { let decoder = JSONDecoder() decoder.userInfo[Self.configurationUserInfoKey] = configuration let wrapper = try decoder.decode(ConfigurationDecodingWrapper.self, from: data) return wrapper.wrapped } } private let configurationUserInfoKey = CodingUserInfoKey(rawValue: "configuration")! ``` ### Partial Decoding Decode only the fields you need: ```swift struct ArticlePreview: Decodable { let id: UUID let title: String // Omit body, comments, etc. } // JSON has many more fields, but we only decode id and title ``` --- ## Part 7: Debugging ### DecodingError Cases ```swift do { let user = try decoder.decode(User.self, from: data) } catch DecodingError.keyNotFound(let key, let context) { print("Missing key '\(key)' at path: \(context.codingPath)") } catch DecodingError.typeMismatch(let type, let context) { print("Type mismatch for \(type) at path: \(context.codingPath)") } catch DecodingError.valueNotFound(let type, let context) { print("Value not found for \(type) at path: \(context.codingPath)") } catch DecodingError.dataCorrupted(let context) { print("Data corrupted at path: \(context.codingPath)") } catch { print("Other error: \(error)") } ``` ### Debugging Techniques **1. Pretty-print JSON** ```swift let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let jsonData = try encoder.encode(user) print(String(data: jsonData, encoding: .utf8)!) ``` **2. Inspect coding path** ```swift // In custom init(from:) print("Decoding at path: \(decoder.codingPath)") ``` **3. Validate JSON structure** ```swift // Quick check: Can it decode as Any? let json = try JSONSerialization.jsonObject(with: data) print(json) // See actual structure ``` --- ## Anti-Patterns | Anti-Pattern | Cost | Better Approach | |--------------|------|-----------------| | **Manual JSON string building** | Injection vulnerabilities, escaping bugs, no type safety | Use `JSONEncoder` | | **`try?` swallowing DecodingError** | Silent failures, debugging nightmares, data loss | Handle specific error cases | | **Optional properties to avoid decode errors** | Runtime crashes, nil checks everywhere, masks structural issues | Fix JSON/model mismatch or use `DecodableWithConfiguration` | | **Duplicating partial models** | 2-5 hours maintenance per change, sync issues, fragile | Use bridge types or configuration | | **Ignoring date timezone** | Intermittent bugs across regions, data corruption | Always use ISO8601 with timezone or explicit UTC | | **`JSONSerialization` for Codable types** | 3x more boilerplate, manual type casting, error-prone | Use `JSONDecoder`/`JSONEncoder` | | **No locale on DateFormatter** | Parsing fails in non-US locales | Set `locale = Locale(identifier: "en_US_POSIX")` | ### Why try? is Dangerous ```swift // ❌ Silent failure - production bug waiting to happen let user = try? JSONDecoder().decode(User.self, from: data) // If this fails, user is nil - why? No idea. // ✅ Explicit error handling do { let user = try JSONDecoder().decode(User.self, from: data) } catch { logger.error("Failed to decode user: \(error)") // Now you know WHY it failed } ``` --- ## Pressure Scenarios ### Scenario 1: "Just Use try? to Make It Compile" **Context**: API integration deadline tomorrow, decoder failing on some edge case. **Pressure**: "We can debug it later, just make it work now." **Why You'll Rationalize**: - "It's only failing on 1% of requests" - "We can add logging later" - "Customers won't notice" **What Actually Happens**: - Silent data loss for that 1% - No logs, so you can't debug in production - Customer complaints 3 months later - You've forgotten the context by then **Discipline Response**: > "Using `try?` here means we'll lose data silently. Let me spend 5 minutes handling the specific error case. If it's truly rare, I'll log it so we can fix the root cause." **5-Minute Fix**: ```swift do { return try decoder.decode(User.self, from: data) } catch DecodingError.keyNotFound(let key, let context) { logger.error("Missing key '\(key)' in API response", metadata: [ "path": .string(context.codingPath.description), "rawJSON": .string(String(data: data, encoding: .utf8) ?? "") ]) throw APIError.invalidResponse(reason: "Missing key: \(key)") } catch { logger.error("Failed to decode User", error: error) throw APIError.decodingFailed(error) } ``` **Result**: You discover the API sometimes omits the `email` field for deleted users. Fix: make `email` optional only for that case, not all users. --- ### Scenario 2: "Dates Are Intermittent, Must Be Server Bug" **Context**: Date parsing works in your timezone but fails for European QA team. **Pressure**: "It works for me, QA must be doing something wrong." **Why You'll Rationalize**: - "My tests pass locally" - "The server is probably sending bad data" - "It's their device settings" **What Actually Happens**: - Server sends dates without timezone: `"2024-12-14T10:00:00"` - Your device (PST) interprets as 10:00 PST - QA device (CET) interprets as 10:00 CET - Different absolute times, intermittent bugs **Discipline Response**: > "Intermittent date failures are almost always timezone issues. Let me check if we're using ISO8601 with timezone offsets." **Check**: ```swift // ❌ Current (fails across timezones) decoder.dateDecodingStrategy = .iso8601 // Server sends: "2024-12-14T10:00:00" (no timezone) // PST device: Dec 14, 10:00 PST // CET device: Dec 14, 10:00 CET // Bug: Different times! // ✅ Fix: Require server to send timezone // "2024-12-14T10:00:00+00:00" // OR: Explicitly parse as UTC decoder.dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) let formatter = ISO8601DateFormatter() formatter.timeZone = TimeZone(secondsFromGMT: 0) // Force UTC guard let date = formatter.date(from: dateString) else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Invalid ISO8601 date: \(dateString)" ) } return date } ``` **Result**: Bug fixed, server adds timezone to API (or you parse explicitly as UTC). No more intermittent failures. --- ### Scenario 3: "Just Make It Optional" **Context**: New API field causes decoding to fail. Product manager wants a fix in 1 hour. **Pressure**: "Can't you just make that field optional? We need this shipped." **Why You'll Rationalize**: - "It's faster than fixing the API" - "We can make it non-optional later" - "Users won't notice" **What Actually Happens**: - Field is actually required for the feature - You add `user.email ?? ""` everywhere - 3 months later: production crash because `email` was nil - Now you can't remember why it was optional **Discipline Response**: > "Making it optional masks the real problem. Let me check if the API is wrong or our model is wrong. This will take 10 minutes." **Investigation**: ```swift // Step 1: Print raw JSON do { let json = try JSONSerialization.jsonObject(with: data) print(json) } catch { print("Invalid JSON: \(error)") } // Step 2: Check if key exists but value is null // {"email": null} vs key missing entirely // Step 3: Check API docs - is email actually required? ``` **Common Outcomes**: 1. **API is wrong**: Field should be there → File bug, get hotfix 2. **Model is wrong**: Field is optional in some flows → Use proper optionality with clear documentation 3. **Structural mismatch**: Field is nested → Use nested container **Result**: You discover `email` is nested in `user.contact.email` in the new API version. Fix with nested container, not optionality. ```swift // ✅ Correct fix struct User: Decodable { let id: UUID let email: String // Still required enum CodingKeys: CodingKey { case id, contact } enum ContactKeys: CodingKey { case email } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) let contact = try container.nestedContainer( keyedBy: ContactKeys.self, forKey: .contact ) email = try contact.decode(String.self, forKey: .email) } } ``` --- ## Related Skills - **swift-concurrency** — Codable types crossing actor boundaries must be `Sendable` - **swiftdata** — `@Model` types use Codable for CloudKit sync - **networking** — `Coder` protocol wraps Codable for Network.framework - **app-intents-ref** — `AppEnum` parameters use Codable serialization --- ## Key Takeaways 1. **Prefer automatic synthesis** — Add `: Codable` when structure matches JSON 2. **Use CodingKeys for simple mismatches** — Rename or exclude without manual code 3. **Manual implementation for structural differences** — Nested containers, bridge types 4. **Always set locale and timezone** — `DateFormatter` requires `en_US_POSIX` and explicit timezone 5. **Never swallow errors with try?** — Handle `DecodingError` cases explicitly 6. **Codable + Sendable** — Value types (structs/enums) are ideal for async networking **Core Principle**: Codable is Swift's universal serialization protocol. Master it once, use it everywhere.