Skip to content
Back to skills

Axiom Codable

ASecurity

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

  • 65 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 9, 2026
developmentgoswiftdebugginggitapiperformancedocumentation

Works with

  • api

Security analysis

A100/100

Scanned October 9, 2026

npx -y skills add ComeOnOliver/skillshub --skill axiom-codable --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Axiom Codable?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Axiom Codable
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/comeonoliver-axiom-codable/badge)](https://www.skillsdirectory.com/skills/comeonoliver-axiom-codable)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
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<Value: StringRepresentable>: 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<Double>

    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<T: DecodableWithConfiguration>: 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<T: DecodableWithConfiguration>(
        _ 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<T>.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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.