codable-patterns について
JSONEncoder と JSONDecoder で型とデータを変換する、Swift 開発者向けの Codable パターンガイドです。カスタム構造、エラー要素を読み飛ばす配列、日付戦略に加え、一般的なストレージやネットワーク API との連携を扱います。
主な機能
- 01Codable 準拠の自動生成とカスタマイズ
- 02ネストした JSON と異種配列のマッピング
- 03日付、データ、キー戦略の設定
- 04配列内の不正要素のスキップ
- 05URLSession、SwiftData、UserDefaults との利用
活用シーン
- 01日付形式が異なる API レスポンスのデコード
- 02snake_case の JSON を Swift プロパティに対応付け
- 03不正な要素を含む配列の読み込み
- 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+ です。
目次
- 基本的な準拠
- カスタム CodingKeys
- カスタムのデコードとエンコード
- ネストされたコンテナとフラット化されたコンテナ
- 異種配列
- 日付のデコード戦略
- データとキーの戦略
- 要素を欠落させる配列のデコード
- 単一値コンテナ
- キーがない場合のデフォルト値
- エンコーダーとデコーダーの設定
- URLSession での Codable
- SwiftData での Codable
- UserDefaults での Codable
- よくある間違い
- 確認用チェックリスト
- 参考資料
基本的な準拠
保存されるすべてのプロパティ自体が 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構造体を使用している
参考資料
- Codable -- Encodable と Decodable を組み合わせたプロトコル
- JSONDecoder -- JSON データを Codable 型にデコードします
- JSONEncoder -- Codable 型を JSON データにエンコードします
- CodingKey -- キーのエンコード/デコードに使うプロトコル
- Encoding and Decoding Custom Types -- カスタム Codable 準拠に関する Apple のガイド
- Using JSON with Custom Types -- JSON のパターンを示す Apple のサンプルコード