swift-codable について
swift-codableは、JSONEncoderやJSONDecoderを使うSwift開発者向けの、Codableのエンコードとデコードに関するスキルです。カスタムマッピングや形式の設定、デコードエラー、API・永続化での扱いを解説します。
主な機能
- 01Codable型のエンコードとデコード
- 02キーとコーディング処理のカスタマイズ
- 03ネスト・フラット化コンテナの処理
- 04日付・データ・キー戦略の設定
- 05欠落キーと配列デコードへの対応
活用シーン
- 01APIのJSONレスポンスのデコード
- 02Swift型のJSONエンコード
- 03キー・日付・デコードエラーの調査
- 04永続化境界でのCodableパターン選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swift-codableインストール後、Claude Code で/swift-codableを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/codable-advanced-patterns.md
詳細
Swift Codable
JSONEncoder、JSONDecoder および関連 API を使用して、Swift の型を
Codable(Encodable & Decodable)でエンコードおよびデコードします。
対象は Swift 6.3/iOS 26 以降です。
目次
- デコードと検証のワークフロー
- 基本的な準拠
- カスタム CodingKeys
- カスタムのデコードとエンコード
- ネストしたコンテナとフラット化したコンテナ
- 異種配列
- 日付のデコード戦略
- Data とキーの戦略
- 損失を許容する配列のデコード
- 単一値コンテナ
- キーが存在しない場合のデフォルト値
- エンコーダーとデコーダーの設定
- URLSession での Codable
- SwiftData での Codable
- UserDefaults での Codable
- よくある間違い
- レビュー用チェックリスト
- 参考資料
デコードと検証のワークフロー
- 成功ケース、キーが存在しないケース、null、形式が不正なケース、略語を含むキー、日付のフィクスチャをデコードします。
- 失敗した場合は、
DecodingError、そのcodingPath、生のペイロードを確認します。 - 不一致があるモデル、キー、コンテナ、または戦略だけを修正します。損失を許容するデコードで契約上の失敗を隠さないでください。
- フィクスチャを再実行し、契約の一部として双方向の処理が求められる場合は、エンコードとデコードの往復も確認します。
基本的な準拠
保存されるプロパティがすべて 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:) も使用してください。
異種配列
識別子に基づく混在配列については、Advanced Codable Patterns を読み込んでください。
日付のデコード戦略
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
Data とキーの戦略
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
キー戦略は、機械的な snake_case から camelCase への変換に限って使用してください。
convertFromSnakeCase は、Swift の略語や頭字語の表記規則ではなく、綴りに基づいて変換します。
image_url、base_uri、user_id に一致するのは、それぞれ imageUrl、baseUri、userId のみです。
Swift のモデルで imageURL、baseURI、userID を使用する場合は、明示的な CodingKeys を宣言してください。この戦略でこれらの名前が合成されることはありません。
損失を許容する配列のデコード
一部のデータを正常に処理できればよいことが製品の契約に含まれる場合に限り、損失を許容する配列を使用してください。Lossy Arrays を読み込んでください。
単一値コンテナ
型安全なプリミティブのラッパーには singleValueContainer() を使用してください。Single-Value Wrappers を参照してください。
キーが存在しない場合のデフォルト値
保存プロパティにデフォルト値を指定しても、合成されたデコードで必須キーの欠落が許容されるわけではありません。欠落または null の値に明示的なフォールバック動作を定める契約の場合は、Missing-Key Defaults を読み込んでください。
エンコーダーとデコーダーの設定
マッチング戦略は、トランスポート/ファイル形式の境界で維持してください。準拠しない浮動小数点数やプロパティリストのガイダンスについては、 エンコーダーの設定 を参照してください。
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. 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
}
SwiftDataでのCodable
スキーマの値は型を維持し、永続化設計はswiftdataに振り分けてください。永続化の境界を参照してください。
UserDefaultsでのCodable
小さな設定にはプリミティブ型を使用してください。小規模なCodable RawRepresentable/@AppStorageの受け渡しについては
永続化の境界
を参照してください。大規模または永続的なデータには、実際の永続化レイヤーを使用してください。
よくある間違い
1. デフォルト値が設定されたフィールドの欠落に対処していない:
// 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. 要素が1つ無効なだけで配列全体の処理に失敗する:
// 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. 日付の戦略が一致していない:
// 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 for simple cases
decoder.keyDecodingStrategy = .convertFromSnakeCase
// Keep CodingKeys for `imageURL`, `baseURI`, `userID`, and similar names.
レビューチェックリスト
- エンコードが不要な場合、型は
Decodableのみに準拠している - オプショナルキーまたは欠落したキーのデフォルト値に
decodeIfPresentを使用している - シンプルなsnake_case APIには
keyDecodingStrategy = .convertFromSnakeCaseを使用し、頭字語の表記にはCodingKeysを保持している -
dateDecodingStrategyがAPIの日付形式と一致している - 信頼性の低いデータの配列では、ロスィー・デコーディングを使用して無効な要素をスキップしている
- カスタム
init(from:)で、デコード後の修正ではなくデータの検証と変換を行っている - 決定的なテスト出力のため、
JSONEncoder.outputFormattingに.sortedKeysを含めている - ラッパー型(UserIDなど)で
singleValueContainerを使用し、すっきりしたJSONにしている - 一貫したAPIエンベロープ処理のため、汎用の
APIResponse<T>ラッパーを使用している - デコードした値を強制アンラップしていない
- 永続化の境界が明確である:SwiftDataは互換性のある非計算モデルプロパティにのみ使用し、
@AppStorage/UserDefaultsは小さなプリミティブ型またはRawRepresentableの設定にのみ使用している
参考資料
- 高度なCodableパターン -- 混在配列、ロスィー・デコーディング、ラッパー、デフォルト値、設定、永続化の境界
- Codable -- EncodableとDecodableを組み合わせたプロトコル
- JSONDecoder -- JSONデータをCodable型にデコードします
- JSONEncoder -- Codable型をJSONデータとしてエンコードします
- CodingKey -- キーのエンコード/デコードに使用するプロトコル
- JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase -- snake-case変換の動作と制限事項
- カスタム型のエンコードとデコード -- カスタムCodable準拠に関するAppleのガイド
- カスタム型でJSONを使用する -- JSONパターンに関するAppleのサンプルコード
- アプリのモデルデータを起動間で保持する -- SwiftDataのモデルプロパティの互換性