mcpField
C
codable-patterns

codable-patterns

github.com/dpearson2699開発1.2k252 回インストール13 週間前に更新
リポジトリ

codable-patterns について

JSONEncoder と JSONDecoder で型とデータを変換する、Swift 開発者向けの Codable パターンガイドです。カスタム構造、エラー要素を読み飛ばす配列、日付戦略に加え、一般的なストレージやネットワーク API との連携を扱います。

主な機能

  1. 01Codable 準拠の自動生成とカスタマイズ
  2. 02ネストした JSON と異種配列のマッピング
  3. 03日付、データ、キー戦略の設定
  4. 04配列内の不正要素のスキップ
  5. 05URLSession、SwiftData、UserDefaults との利用

活用シーン

  1. 01日付形式が異なる API レスポンスのデコード
  2. 02snake_case の JSON を Swift プロパティに対応付け
  3. 03不正な要素を含む配列の読み込み
  4. 04SwiftData または UserDefaults への Codable 値の保存

インストール

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

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill codable-patterns

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

詳細

Codable パターン

JSONEncoder、JSONDecoder、および関連 API を使用して、Swift の型を Codable(Encodable & Decodable)でエンコードおよびデコードします。対象は Swift 6.2 / iOS 26+ です。

目次

基本的な準拠

保存されるすべてのプロパティ自体が Codable に準拠している場合、コンパイラーが自動的に準拠を合成します。

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)

読み取り専用の API レスポンスには Decodable を、書き込み専用には Encodable を優先してください。両方向が必要な場合にのみ Codable を使用してください。

カスタム CodingKeys

CodingKeys 列挙型を宣言すると、カスタムデコーダーを記述せずに JSON キーの名前を変更できます。

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

保存されるすべてのプロパティを列挙型に含める必要があります。プロパティを CodingKeys から省略すると、エンコード/デコードの対象外になります。デフォルト値を指定するか、別途計算してください。

カスタムのデコードとエンコード

合成された準拠では処理できない変換を行うには、init(from:) と encode(to:) をオーバーライドします。

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

ネストされたコンテナとフラット化されたコンテナ

nestedContainer(keyedBy:forKey:) を使用して、ネストされた JSON をたどり、フラット化します。

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

複数の nestedContainer 呼び出しを連結すると、深くネストされた構造をフラット化できます。ネストされた配列には nestedUnkeyedContainer(forKey:) も使用します。

異種配列

識別子フィールドを使用して、異なる型が混在する配列をデコードします。

// JSON: [{"type":"text","content":"Hello"},{"type":"image","url":"pic.jpg"}]
enum ContentBlock: Decodable {
    case text(String)
    case image(URL)

    enum CodingKeys: String, CodingKey { case type, content, url }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        switch type {
        case "text":
            let content = try container.decode(String.self, forKey: .content)
            self = .text(content)
        case "image":
            let url = try container.decode(URL.self, forKey: .url)
            self = .image(url)
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type, in: container,
                debugDescription: "Unknown type: \(type)")
        }
    }
}

let blocks = try JSONDecoder().decode([ContentBlock].self, from: jsonData)

日付のデコード戦略

API に合わせて JSONDecoder.dateDecodingStrategy を設定します。

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

JSONEncoder にも対応する戦略を設定します。 encoder.dateEncodingStrategy = .iso8601

データとキーの戦略

let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64           // Base64-encoded Data fields
decoder.keyDecodingStrategy = .convertFromSnakeCase  // snake_case -> camelCase
// {"user_name": "Alice"} maps to `var userName: String` -- no CodingKeys needed

let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.keyEncodingStrategy = .convertToSnakeCase

要素を欠落させる配列のデコード

デフォルトでは、無効な要素が 1 つでもあると配列全体のデコードに失敗します。ラッパーを使用して、無効な要素をスキップします。

struct LossyArray<Element: Decodable>: Decodable {
    let elements: [Element]

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        var elements: [Element] = []
        while !container.isAtEnd {
            if let element = try? container.decode(Element.self) {
                elements.append(element)
            } else {
                _ = try? container.decode(AnyCodableValue.self) // advance past bad element
            }
        }
        self.elements = elements
    }
}
private struct AnyCodableValue: Decodable {}

単一値コンテナ

singleValueContainer() を使用してプリミティブ型をラップし、型安全性を高めます。

struct UserID: Codable, Hashable {
    let rawValue: String

    init(_ rawValue: String) { self.rawValue = rawValue }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        rawValue = try container.decode(String.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
}
// JSON: "usr_abc123" decodes directly to UserID

キーがない場合のデフォルト値

decodeIfPresent と nil 合体演算子を使用して、デフォルト値を指定します。

struct Settings: Decodable {
    let theme: String
    let fontSize: Int
    let notificationsEnabled: Bool

    enum CodingKeys: String, CodingKey {
        case theme, fontSize = "font_size"
        case notificationsEnabled = "notifications_enabled"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        theme = try container.decodeIfPresent(String.self, forKey: .theme) ?? "system"
        fontSize = try container.decodeIfPresent(Int.self, forKey: .fontSize) ?? 16
        notificationsEnabled = try container.decodeIfPresent(
            Bool.self, forKey: .notificationsEnabled) ?? true
    }
}

エンコーダーとデコーダーの設定

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]

// Non-conforming floats (NaN, Infinity are not valid JSON)
encoder.nonConformingFloatEncodingStrategy = .convertToString(
    positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")
decoder.nonConformingFloatDecodingStrategy = .convertFromString(
    positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")
PropertyListEncoder / PropertyListDecoder
let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = .xml  // or .binary
let data = try plistEncoder.encode(settings)
let decoded = try PropertyListDecoder().decode(Settings.self, from: data)

URLSession での Codable

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 for wrapped responses
struct APIResponse<T: Decodable>: Decodable {
    let data: T
    let meta: Meta?
    struct Meta: Decodable { let page: Int; let totalPages: Int }
}
let users = try decoder.decode(APIResponse<[User]>.self, from: data).data

SwiftData での Codable

Codable 構造体は、SwiftData モデルの複合属性として機能します。iOS 18 以降では、明示的な @Attribute(.transformable) を指定しなくても、SwiftData がネイティブでサポートします。

struct Address: Codable {
    var street: String
    var city: String
    var zipCode: String
}

@Model class Contact {
    var name: String
    var address: Address?  // Codable struct stored as composite attribute
    init(name: String, address: Address? = nil) {
        self.name = name; self.address = address
    }
}

UserDefaults での Codable

RawRepresentable を介して Codable 値を保存し、@AppStorage で使用します。

struct UserPreferences: Codable {
    var showOnboarding: Bool = true
    var accentColor: String = "blue"
}

extension UserPreferences: RawRepresentable {
    init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let decoded = try? JSONDecoder().decode(Self.self, from: data)
        else { return nil }
        self = decoded
    }
    var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let string = String(data: data, encoding: .utf8)
        else { return "{}" }
        return string
    }
}

struct SettingsView: View {
    @AppStorage("userPrefs") private var prefs = UserPreferences()
    var body: some View {
        Toggle("Show Onboarding", isOn: $prefs.showOnboarding)
    }
}

よくある間違い

1. 存在しないオプショナルキーを処理しない:

// DON'T -- crashes if key is absent
let value = try container.decode(String.self, forKey: .bio)
// DO -- returns nil for missing keys
let value = try container.decodeIfPresent(String.self, forKey: .bio) ?? ""

2. 無効な要素が 1 つあるだけで配列全体の処理に失敗する:

// DON'T -- one bad element kills the whole decode
let items = try container.decode([Item].self, forKey: .items)
// DO -- use LossyArray or decode elements individually
let items = try container.decode(LossyArray<Item>.self, forKey: .items).elements

3. 日付戦略の不一致:

// 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. デコードしたオプショナルを強制アンラップする:

// 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. Decodable だけで十分な場合に Codable を使用する:

// 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. 単純な snake_case API に手動で CodingKeys を指定する:

// 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
decoder.keyDecodingStrategy = .convertFromSnakeCase

レビューチェックリスト

  • エンコードが不要な場合に限り、型を Decodable のみに準拠させている
  • オプショナルなキーまたは欠落しているキーには、デフォルト値を設定して decodeIfPresent を使用している
  • 単純なスネークケースの API では、手動の CodingKeys ではなく keyDecodingStrategy = .convertFromSnakeCase を使用している
  • dateDecodingStrategy が API の日付形式に一致している
  • 信頼性の低いデータの配列では、無効な要素をスキップする損失許容デコードを使用している
  • カスタムの init(from:) で、デコード後に修正するのではなく、データを検証して変換している
  • テスト出力を決定的にするため、JSONEncoder.outputFormatting に .sortedKeys を含めている
  • ラッパー型(UserID など)で、クリーンな JSON のために singleValueContainer を使用している
  • API エンベロープを一貫して処理するため、汎用の APIResponse<T> ラッパーを使用している
  • デコードした値を強制アンラップしていない
  • @AppStorage の Codable 型が RawRepresentable に準拠している
  • SwiftData の複合属性で Codable 構造体を使用している

参考資料

コメント