evals/evals.json
{
"skill_name": "swift-codable",
"evals": [
{
"id": 0,
"prompt": "Review and correct this Codable API plan for an iOS 26 app: the team made every response model Codable, uses JSONDecoder().decode before checking HTTP status, configures .convertFromSnakeCase and expects image_url to populate imageURL automatically, decodes ISO 8601 timestamps with the default Date strategy, and decodes a wrapped response with `decoder` outside the function where it was declared. Give concise corrected Swift examples where useful. Explicitly correct every read-only API response model to Decodable, not Codable; state that image_url maps to imageUrl, not imageURL; and note that the envelope helper needs its own decoder because another function's decoder is out of scope.",
"expected_output": "A correction-focused Codable review that uses Decodable, not Codable, for read-only responses, validates HTTP status before decoding, configures date and key strategies in scope, handles imageURL with CodingKeys because image_url maps to imageUrl, and decodes a generic API envelope with a decoder declared in the same helper.",
"files": [],
"assertions": [
"Uses `Decodable` rather than `Codable` for read-only API response models.",
"Validates `HTTPURLResponse` status before attempting to decode the body.",
"Configures `JSONDecoder.dateDecodingStrategy = .iso8601` for ISO 8601 date strings.",
"Explains that `.convertFromSnakeCase` maps `image_url` to `imageUrl`, not `imageURL`, and uses `CodingKeys` for that field.",
"Shows the generic envelope decode with a decoder variable that is declared in the same helper where it is used."
]
},
{
"id": 1,
"prompt": "A teammate says we can delete all CodingKeys because `decoder.keyDecodingStrategy = .convertFromSnakeCase` will handle `user_id`, `image_url`, `base_uri`, and `display_name`. Review that claim and show the model code you would keep.",
"expected_output": "A key-strategy review that keeps automatic snake_case conversion for simple lower-camel properties while retaining CodingKeys for URL, URI, and ID initialism spellings that conversion cannot infer.",
"files": [],
"assertions": [
"States that `.convertFromSnakeCase` is suitable for simple snake_case to lower-camel mappings such as `display_name` to `displayName`.",
"States that `.convertFromSnakeCase` cannot infer initialism capitalization such as URL, URI, or ID.",
"Uses explicit `CodingKeys` for fields like `imageURL`, `baseURI`, or `userID`.",
"Avoids recommending manual CodingKeys for every simple snake_case property when a decoder strategy is enough."
]
},
{
"id": 2,
"prompt": "Review this storage plan: a SwiftData `@Model` has an optional `Address` struct that conforms to Codable, and a SwiftUI settings screen stores a `UserPreferences` Codable struct in `@AppStorage`. The draft says Codable value types only work in SwiftData on iOS 18+ and proposes transformable/blob workarounds. The @AppStorage type can rely on synthesized Codable defaults if the stored JSON is `{}`. Give corrected boundary-aware guidance. State clearly that SwiftData persists compatible noncomputed stored properties declared on @Model types, including Codable structs and enums when they are part of the durable model schema. Do not propose @Attribute(.transformable), encoded Data, or encoded String blob storage as a fallback; use typed SwiftData properties and a complete RawRepresentable JSON String bridge for @AppStorage, including init?(rawValue:) and rawValue self-encoding/decoding.",
"expected_output": "A boundary-aware Codable storage review that uses source-backed SwiftData compatibility wording for compatible noncomputed @Model properties, avoids unsupported iOS 18+/transformable/blob fallback claims, stores @AppStorage values through a complete RawRepresentable JSON String raw-value bridge, and explains missing-key defaults need custom decoding or tolerant rawValue handling.",
"files": [],
"assertions": [
"States that @Model types can persist compatible noncomputed stored properties and explicitly includes Codable structs or enums, without asserting an unsupported iOS 18+ threshold.",
"Does not propose `@Attribute(.transformable)`, encoded `Data`, or encoded `String` blob storage as a fallback for the SwiftData Codable value property.",
"Uses a complete `RawRepresentable` JSON `String` raw-value bridge for storing a Codable value with `@AppStorage`.",
"Explains that synthesized `Decodable` does not use stored property default values when a nonoptional key is missing, so `{}` needs custom decoding or fallback handling.",
"Keeps the answer focused on Codable integration boundaries rather than expanding into full SwiftData schema design or SwiftUI state management."
]
}
]
}
references/codable-advanced-patterns.md
# Advanced Codable Patterns
Use these patterns after the model's core keys, nested containers, and decoding
strategies are established.
## Contents
- [Heterogeneous Arrays](#heterogeneous-arrays)
- [Lossy Arrays](#lossy-arrays)
- [Single-Value Wrappers](#single-value-wrappers)
- [Missing-Key Defaults](#missing-key-defaults)
- [Encoder Configuration](#encoder-configuration)
- [Persistence Boundaries](#persistence-boundaries)
## Heterogeneous Arrays
Decode a discriminator first, then decode only fields owned by that case:
```swift
enum ContentBlock: Decodable {
case text(String)
case image(URL)
enum CodingKeys: String, CodingKey { case type, content, url }
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
switch try values.decode(String.self, forKey: .type) {
case "text": self = .text(try values.decode(String.self, forKey: .content))
case "image": self = .image(try values.decode(URL.self, forKey: .url))
default:
throw DecodingError.dataCorruptedError(
forKey: .type, in: values,
debugDescription: "Unknown content type"
)
}
}
}
```
## Lossy Arrays
Default array decoding fails when any element is invalid. Use lossy decoding
only when the product contract permits partial data, and report skipped items.
```swift
struct LossyArray<Element: Decodable>: Decodable {
let elements: [Element]
init(from decoder: Decoder) throws {
var values = try decoder.unkeyedContainer()
var decoded: [Element] = []
while !values.isAtEnd {
if let value = try? values.decode(Element.self) {
decoded.append(value)
} else {
_ = try? values.decode(DiscardedValue.self)
}
}
elements = decoded
}
}
private struct DiscardedValue: Decodable {}
```
## Single-Value Wrappers
```swift
struct UserID: Codable, Hashable {
let rawValue: String
init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
func encode(to encoder: Encoder) throws {
var value = encoder.singleValueContainer()
try value.encode(rawValue)
}
}
```
## Missing-Key Defaults
A stored property default does not make synthesized decoding tolerate a missing
nonoptional key. Decode manually when missing or null has an explicit fallback:
```swift
let values = try decoder.container(keyedBy: CodingKeys.self)
theme = try values.decodeIfPresent(String.self, forKey: .theme) ?? "system"
```
Preserve the distinction between a missing key, explicit null, and malformed
data when the API contract assigns them different meanings.
## Encoder Configuration
Configure matching date, data, float, and key strategies once per transport or
file format. Use `PropertyListEncoder`/`PropertyListDecoder` for property lists;
do not send plist configuration through JSON helpers.
## Persistence Boundaries
- SwiftData: persist supported Codable value types as typed model properties;
route schema and unsupported-storage decisions to `swiftdata`.
- UserDefaults: store primitive preferences directly. For a small Codable
preference, a `RawRepresentable` wrapper with JSON-string storage can support
`@AppStorage`; route larger or durable data to a real persistence layer.
SKILL.md
---
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-and-verify-workflow)
- [Basic Conformance](#basic-conformance)
- [Custom CodingKeys](#custom-codingkeys)
- [Custom Decoding and Encoding](#custom-decoding-and-encoding)
- [Nested and Flattened Containers](#nested-and-flattened-containers)
- [Heterogeneous Arrays](#heterogeneous-arrays)
- [Date Decoding Strategies](#date-decoding-strategies)
- [Data and Key Strategies](#data-and-key-strategies)
- [Lossy Array Decoding](#lossy-array-decoding)
- [Single Value Containers](#single-value-containers)
- [Default Values for Missing Keys](#default-values-for-missing-keys)
- [Encoder and Decoder Configuration](#encoder-and-decoder-configuration)
- [Codable with URLSession](#codable-with-urlsession)
- [Codable with SwiftData](#codable-with-swiftdata)
- [Codable with UserDefaults](#codable-with-userdefaults)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Decode and Verify Workflow
1. Decode representative success, missing, null, malformed, acronym-key, and
date fixtures.
2. On failure, inspect `DecodingError`, its `codingPath`, and the raw payload.
3. Correct only the mismatched model, key, container, or strategy; do not hide
contract failures with lossy decoding.
4. 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:
```swift
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:
```swift
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:
```swift
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)
// 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](references/codable-advanced-patterns.md#heterogeneous-arrays)
for discriminator-based mixed arrays.
## Date Decoding Strategies
Configure `JSONDecoder.dateDecodingStrategy` to match your API:
```swift
let 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
```swift
let 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](references/codable-advanced-patterns.md#lossy-arrays).
## Single Value Containers
Use `singleValueContainer()` for type-safe primitive wrappers; see
[Single-Value Wrappers](references/codable-advanced-patterns.md#single-value-wrappers).
## Default Values for Missing Keys
Stored defaults do not make synthesized decoding tolerate missing nonoptional
keys. Load [Missing-Key Defaults](references/codable-advanced-patterns.md#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](references/codable-advanced-patterns.md#encoder-configuration)
for nonconforming floats and property-list guidance.
## Codable with URLSession
```swift
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)
}
// 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](references/codable-advanced-patterns.md#persistence-boundaries).
## Codable with UserDefaults
Use primitives for small preferences. Load
[Persistence Boundaries](references/codable-advanced-patterns.md#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 `Decodable` only when encoding is not needed
- [ ] `decodeIfPresent` used with defaults for optional or missing keys
- [ ] `keyDecodingStrategy = .convertFromSnakeCase` used for simple snake_case APIs, with CodingKeys retained for acronym spellings
- [ ] `dateDecodingStrategy` matches 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.outputFormatting` includes `.sortedKeys` for deterministic test output
- [ ] Wrapper types (UserID, etc.) use `singleValueContainer` for 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 or `RawRepresentable` preferences
## References
- [Advanced Codable patterns](references/codable-advanced-patterns.md) -- mixed arrays, lossy decoding, wrappers, defaults, configuration, and persistence boundaries
- [Codable](https://sosumi.ai/documentation/swift/codable/) -- protocol combining Encodable and Decodable
- [JSONDecoder](https://sosumi.ai/documentation/foundation/jsondecoder/) -- decodes JSON data into Codable types
- [JSONEncoder](https://sosumi.ai/documentation/foundation/jsonencoder/) -- encodes Codable types as JSON data
- [CodingKey](https://sosumi.ai/documentation/swift/codingkey/) -- protocol for encoding/decoding keys
- [JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase](https://sosumi.ai/documentation/foundation/jsondecoder/keydecodingstrategy-swift.enum/convertfromsnakecase) -- snake-case conversion behavior and limitations
- [Encoding and Decoding Custom Types](https://sosumi.ai/documentation/foundation/encoding-and-decoding-custom-types/) -- Apple guide on custom Codable conformance
- [Using JSON with Custom Types](https://sosumi.ai/documentation/foundation/archives_and_serialization/using_json_with_custom_types/) -- Apple sample code for JSON patterns
- [Preserving your app's model data across launches](https://sosumi.ai/documentation/swiftdata/preserving-your-apps-model-data-across-launches) -- SwiftData model property compatibility