mcpField
A
axiom-codable

axiom-codable

github.com/charleswiltgen開発1.2k173 回インストール今日更新
リポジトリ

axiom-codable について

JSON と PropertyList のエンコード、デコードを扱う Swift 6.x 開発者向けの Codable リファレンススキルです。自動合成、キーのカスタマイズ、手動変換、日付処理、デコードエラーの診断を解説します。

主な機能

  1. 01Codable の自動合成と列挙型
  2. 02CodingKeys によるキー設定
  3. 03コンテナとブリッジ型の変換
  4. 04日付デコード戦略
  5. 05デコードエラーの診断

活用シーン

  1. 01異なる名前の JSON キーを対応付け
  2. 02ネストまたは構造が異なるデータをデコード
  3. 03API の日付と型の揺れに対応
  4. 04Codable のデコード失敗を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-codable

インストール後、Claude Code で/axiom-codableを入力して読み込まれたか確認します。

詳細

Swift Codable のパターン

Swift 6.x における JSON および PropertyList のエンコード/デコードのための Codable プロトコル準拠に関する包括的なガイドです。

クイックリファレンス

判断ツリー:各アプローチを使用する場面
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
よくあるエラーのきっかけ
エラー解決策
"Type 'X' does not conform to protocol 'Decodable'"すべての保存プロパティが Codable に準拠していることを確認します
"No value associated with key X"CodingKeys が JSON のキーと一致していることを確認します
"Expected to decode X but found Y instead"型が一致していません。JSON の構造を確認するか、ブリッジ型を使用します
"keyNotFound"JSON に想定されるキーがありません。プロパティをオプショナルにするか、デフォルト値を指定します
"Date parsing failed"デコーダーで dateDecodingStrategy を設定します

パート 1:自動合成

すべての保存プロパティが Codable に準拠している場合、Swift は Codable 準拠を自動的に合成します。

構造体の合成
// ✅ Automatic synthesis
struct User: Codable {
    let id: UUID              // Codable
    var name: String          // Codable
    var membershipPoints: Int // Codable
}

// JSON: {"id":"...", "name":"Alice", "membershipPoints":100}

要件:

  • すべての保存プロパティが Codable に準拠している必要があります
  • プロパティには標準の Swift 型またはその他の Codable 型を使用します
  • カスタム初期化ロジックは不要です
列挙型の合成パターン
パターン 1:Raw Value を持つ列挙型
enum Direction: String, Codable {
    case north, south, east, west
}

// Encodes as: "north"

Raw Value 自体が JSON 表現になります。

パターン 2:Associated Value のない列挙型
enum Status: Codable {
    case success
    case failure
    case pending
}

// Encodes as: {"success":{}}

各ケースは、ケース名をキー、空の辞書を値とするオブジェクトになります。

パターン 3:Associated Value を持つ列挙型
enum APIResult: Codable {
    case success(data: String, count: Int)
    case error(code: Int, message: String)
}

// success case encodes as:
// {"success":{"data":"example","count":5}}

注意点:ラベルのない Associated Value では、_0、_1 のキーが生成されます。

enum Command: Codable {
    case store(String, Int)  // ❌ Unlabeled
}

// Encodes as: {"store":{"_0":"value","_1":42}}

修正方法:予測可能な JSON にするため、Associated Value には常にラベルを付けます。

enum Command: Codable {
    case store(key: String, value: Int)  // ✅ Labeled
}

// Encodes as: {"store":{"key":"value","value":42}}
自動合成が機能しない場合

次の場合、自動合成は失敗します。

  1. 計算プロパティ - エンコードされるのは保存プロパティのみです
  2. Codable に準拠しないプロパティ - Codable 準拠のないカスタム型
  3. プロパティラッパー - @Published、@State(Codable 型を使用する @AppStorage を除く)
  4. クラスの継承 - サブクラスでは init(from:) を手動で実装する必要があります

パート 2:CodingKeys のカスタマイズ

CodingKeys 列挙型を使うと、すべてを手動で実装することなく、エンコード/デコードをカスタマイズできます。

キーの名前変更
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":"..."}
プロパティの除外

プロパティを CodingKeys に含めないことで、エンコード/デコードから除外できます。

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
    }
}

ルール:除外するプロパティにはデフォルト値が必要です。そうでない場合は、init(from:) を手動で実装する必要があります。

スネークケースへの変換

一貫した snake_case → camelCase の変換には、次を使用します。

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// JSON: {"first_name":"Alice", "last_name":"Smith"}
// Decodes to: User(firstName: "Alice", lastName: "Smith")
Enum の Associated Value キー

{CaseName}CodingKeys を使って、Enum の Associated Value のキーをカスタマイズします。

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}}

パターン:ケース名を大文字で始めた {CaseName}CodingKeys を使用します。


パート 3:手動実装

JSON と Swift のモデル間で構造に違いがある場合は、init(from:) と encode(to:) を実装します。

コンテナの型
コンテナ使用する場面
キー付き文字列キーを持つ辞書のようなデータ
非キー付き順序に従う配列のようなデータ
単一値単一の値としてエンコードされるラッパー型
ネスト階層構造の JSON
ネストしたコンテナの例

階層構造の JSON を平坦化します。

// 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)
    }
}
構造の不一致に対応するブリッジ型

JSON の構造が 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
    }
}

パート 4:日付の処理

組み込みのストラテジー
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 の注意点

デフォルト:2024-02-15T17:00:00+01:00
タイムゾーンが必須:タイムゾーンオフセットがない場合、地域によってはデコードに失敗することがあります

// ❌ No timezone - parsing depends on device locale
"2024-02-15T17:00:00"

// ✅ With timezone - unambiguous
"2024-02-15T17:00:00+01:00"
パフォーマンスに関する考慮事項

カスタムクロージャーは日付ごとに実行されます。負荷の高い処理を最適化してください。

// ❌ 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
}

パート 5:型変換

StringBacked ラッパー

数値を文字列としてエンコードする API に対応します。

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)
型の強制変換

異なる型を返すことがある、型付けが緩い API には次を使用します。

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"
            )
        }
    }
}

警告:API の戻り値が本当に予測不能な場合を除き、このパターンは避けてください。厳密な型を優先します。


パート 6:高度なパターン

DecodableWithConfiguration(iOS 15 以降)

JSON にはないデータが必要な型には、次を使用します。

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 を使った回避策(iOS 15~16)
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")!
部分デコード

必要なフィールドだけをデコードします:

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

パート 7: デバッグ

DecodingError のケース
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)")
}
デバッグ手法

1. JSON を整形して出力する

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(user)
print(String(data: jsonData, encoding: .utf8)!)

2. コーディングパスを確認する

// In custom init(from:)
print("Decoding at path: \(decoder.codingPath)")

3. JSON 構造を検証する

// Quick check: Can it decode as Any?
let json = try JSONSerialization.jsonObject(with: data)
print(json)  // See actual structure

アンチパターン

アンチパターンコストより良いアプローチ
手動での JSON 文字列構築インジェクション脆弱性、エスケープのバグ、型安全性の欠如JSONEncoder を使用する
try? で DecodingError を握りつぶすサイレント障害、デバッグの悪夢、データ損失特定のエラーケースを処理する
デコードエラーを避けるためのオプショナルプロパティ実行時クラッシュ、至る所の nil チェック、構造的な問題の隠蔽JSON とモデルの不一致を修正するか、DecodableWithConfiguration を使用する
部分モデルの重複変更ごとに 2〜5 時間のメンテナンス、同期の問題、脆弱性ブリッジ型または構成を使用する
日付のタイムゾーンの無視地域をまたぐ断続的なバグ、データ破損常にタイムゾーン付きの ISO8601 または明示的な UTC を使用する
Codable 型に対する JSONSerialization3 倍のボイラープレート、手動の型キャスト、エラーが発生しやすいJSONDecoder/JSONEncoder を使用する
DateFormatter にロケールがない米国以外のロケールでパースが失敗するlocale = Locale(identifier: "en_US_POSIX") を設定する
try? が危険な理由
// ❌ 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
}

プレッシャーシナリオ

シナリオ 1: 「コンパイルを通すために try? を使えばいい」

状況: API 統合の締め切りが明日、デコーダーが一部のエッジケースで失敗している。

プレッシャー: 「後でデバッグすればいい、今はとにかく動かしてくれ。」

あなたが正当化してしまう理由:

  • 「リクエストの 1% でしか失敗していない」
  • 「後でログを追加すればいい」
  • 「顧客は気づかないだろう」

実際に起こること:

  • その 1% でサイレントなデータ損失が発生する
  • ログがないため、本番環境でデバッグできない
  • 3 か月後に顧客からの苦情が来る
  • その頃にはコンテキストを忘れている

規律ある対応:

「ここで try? を使うと、データをサイレントに失うことになります。特定のエラーケースの処理に 5 分だけかけさせてください。本当に稀なケースなら、根本原因を修正できるようログを出力します。」

5 分でできる修正:

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)
}

結果: API が削除済みユーザーの email フィールドを省略することがあると判明。修正: email をすべてのユーザーではなく、そのケースに限ってオプショナルにする。


シナリオ 2: 「日付が断続的に失敗する、サーバーのバグに違いない」

状況: 日付のパースはあなたのタイムゾーンでは動作するが、ヨーロッパの QA チームでは失敗する。

プレッシャー: 「自分では動いている、QA が何か間違っているに違いない。」

あなたが正当化してしまう理由:

  • 「ローカルではテストが通っている」
  • 「サーバーが不正なデータを送っているのだろう」
  • 「彼らのデバイス設定の問題だ」

実際に起こること:

  • サーバーがタイムゾーンなしで日付を送信する: "2024-12-14T10:00:00"
  • あなたのデバイス (PST) は 10:00 PST と解釈する
  • QA のデバイス (CET) は 10:00 CET と解釈する
  • 異なる絶対時刻になり、断続的なバグが発生する

規律ある対応:

「断続的な日付の失敗は、ほとんどの場合タイムゾーンの問題です。タイムゾーンオフセット付きの ISO8601 を使用しているか確認させてください。」

確認:

// ❌ 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
}

結果: バグが修正され、サーバーが API にタイムゾーンを追加する (または明示的に UTC としてパースする)。断続的な失敗はなくなる。


シナリオ 3:「とりあえずオプショナルにする」

状況:新しい API フィールドが原因でデコードに失敗しています。プロダクトマネージャーは 1 時間以内の修正を求めています。

プレッシャー:「そのフィールドをオプショナルにするだけではだめですか?これをリリースする必要があります。」

合理化してしまう理由:

  • 「API を修正するより早い」
  • 「あとで非オプショナルに戻せばいい」
  • 「ユーザーは気づかない」

実際に起こること:

  • そのフィールドは機能に実際に必要です
  • あらゆる場所に user.email ?? "" を追加します
  • 3 か月後:email が nil のため、本番環境でクラッシュします
  • なぜオプショナルにしたのか、もう思い出せません

規律ある対応:

「オプショナルにすると、本当の問題が隠れてしまいます。API とモデルのどちらが間違っているのか確認させてください。10 分ほどかかります。」

調査:

// 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?

よくある結果:

  1. API が間違っている:フィールドがあるべきです → バグを報告し、緊急修正を依頼します
  2. モデルが間違っている:一部のフローではフィールドがオプショナルです → 明確なドキュメントを添えて、適切にオプショナルとして扱います
  3. 構造が一致していない:フィールドがネストされています → ネストされたコンテナーを使います

結果:新しい API バージョンでは、email が user.contact.email の中にあることがわかります。オプショナルにするのではなく、ネストされたコンテナーで修正します。

// ✅ 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)
    }
}

関連スキル

  • swift-concurrency — アクター境界を越える Codable 型は Sendable である必要があります
  • swiftdata — @Model 型は CloudKit 同期に Codable を使用します
  • networking — Coder プロトコルは Network.framework 向けに Codable をラップします
  • app-intents-ref — AppEnum パラメーターは Codable シリアライズを使用します

重要なポイント

  1. 自動合成を優先する — 構造が JSON と一致する場合は : Codable を追加します
  2. 単純な不一致には CodingKeys を使う — 手動でコードを書かずに名前を変更したり、除外したりできます
  3. 構造の違いには手動実装を使う — ネストされたコンテナーやブリッジ型に対応します
  4. ロケールとタイムゾーンを必ず設定する — DateFormatter には en_US_POSIX と明示的なタイムゾーンが必要です
  5. try? でエラーを握りつぶさない — DecodingError のケースを明示的に処理します
  6. Codable + Sendable — 値型(struct/enum)は非同期ネットワーキングに最適です

基本原則:Codable は Swift の汎用シリアライズプロトコルです。一度習得すれば、どこでも活用できます。

コメント