この記事のすべてのコードを含むSwift Playgroundをダウンロードしてください。

Codableは、古いNSCoding API を置き換えることを目的として 、Swift 4 で導入されました。 NSCodingとは異なり、 CodableはファーストクラスのJSONサポートを備えているため、JSON APIを使用するための有望なオプションになります。
CodableはNSCodingとして素晴らしいです。 完全に制御する必要があるローカルデータをエンコードまたはデコードする必要がある場合は、 自動エンコードおよびデコードを利用できます。
現実の世界では、すべてが非常に複雑で高速になります。 JSONを管理し、すべての製品要件をモデル化するフェイルセーフシステムを構築しようとすることは問題です。
Codableの主な欠点の1つは、カスタムデコードロジックが必要になるとすぐに(単一のキーであっても)、すべてを提供する必要があることです:すべてのエンコードキーを手動で定義し、 init(デコーダー:デコーダー)スローを完全に手動で実装します 。 これは完璧ではありません。 しかし、これは少なくともSwiftでサードパーティのJSONライブラリを使用するのと同じくらい(または悪い)です。 構築されたライブラリにそのようなものが存在することは、すでに勝利です。
したがって、アプリケーションでCodableの使用を開始する場合(およびその主要機能すべてに既に精通している場合)、役に立つと思われるヒントと例を以下に示します。
- 配列の安全なデコード
- 識別子タイプと単一値コンテナ
- 転送の安全なデコード
- いくつかの手動デコード
- 特定のタイプのパラメーターを取り除く
- 別のデコードスキームを使用する
- エンコーディングパッチオプション
配列の安全なデコード
アプリケーションに投稿のコレクションをダウンロードして表示するとします。 各投稿には、 ID (必須)、 タイトル (必須)、 サブタイトル (オプション)があります。
final class Post: Decodable { let id: Id<Post> // More about this type later. let title: String let subtitle: String? }
Postクラスは要件を適切にモデル化します。 Decodableプロトコルを既に受け入れているため、いくつかのデータをデコードする準備ができています。
[ { "id": "pos_1", "title": "Codable: Tips and Tricks" }, { "id": "pos_2" } ]
do { let posts = try JSONDecoder().decode([Post].self, from: json.data(using: .utf8)!) } catch { print(error) //prints "No value associated with key title (\"title\")." }
予想どおり、2番目の投稿オブジェクトにはタイトルがないため、 .keyNotFoundエラーが発生しました。
データが予期される形式と一致しない場合(たとえば、誤解、リグレッション、または予期しないユーザー入力の結果である場合)、システムは自動的にエラーを報告して、開発者が修正できるようにする必要があります。 Swiftは、デコードが失敗するといつでもDecodingErrorの形式で詳細なエラーレポートを提供します。これは非常に便利です。
ほとんどの場合、破損した投稿の1つが他の完全に正しい投稿のページ全体を表示することを妨げることを望んでいません。 これを避けるために、オブジェクトを安全にデコードできる特別なタイプSafe <T>を使用します。 デコード中にエラーを検出すると、安全なエラー検出モードに入り、レポートを送信します。
public struct Safe<Base: Decodable>: Decodable { public let value: Base? public init(from decoder: Decoder) throws { do { let container = try decoder.singleValueContainer() self.value = try container.decode(Base.self) } catch { assertionFailure("ERROR: \(error)") // TODO: automatically send a report about a corrupted data self.value = nil } } }
これで、配列をデコードするときに、1つの要素が破損した場合にデコードを停止したくないことを示すことができます。
do { let posts = try JSONDecoder().decode([Safe<Post>].self, from: json.data(using: .utf8)!) print(posts[0].value!.title) // prints "Codable: Tips and Tricks" print(posts[1].value) // prints "nil" } catch { print(error) }
デコードのデコード([Safe <Post>] .self、from:...は、データに配列が含まれていない場合にエラーを引き起こすことに注意してください。一般に、このようなエラーはより高いレベルでキャッチする必要があります。返す要素がない場合は常に空の配列を返します。
識別子タイプと単一値コンテナ
前の例では、特別な識別子ID <Post>を使用しました。 Type Idは、実際にはId自体では使用されないが、異なるタイプのIdを比較するためにコンパイラーによって使用される一般的なパラメーターEntityを使用してパラメーターを受け取ります。 したがって、コンパイラは、Id <Image>が予期される場所で、誤ってId <Media>を渡さないようにします。
SwiftのクライアントAPIの記事で、型を使用するときのセキュリティのためにファントム型も使用しました 。
Idタイプ自体は非常にシンプルで、元のStringの単なるラッパーです:
public struct Id<Entity>: Hashable { public let raw: String public init(_ raw: String) { self.raw = raw } public var hashValue: Int { return raw.hashValue } public static func ==(lhs: Id, rhs: Id) -> Bool { return lhs.raw == rhs.raw } }
Codableを追加するのは間違いなく困難な作業です。 これには、特別なタイプのSingleValueEncodingContainerが必要です 。
単一のブロックされていない値のストレージと直接エンコードをサポートできるコンテナ。
extension Id: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let raw = try container.decode(String.self) if raw.isEmpty { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Cannot initialize Id from an empty string" ) } self.init(raw) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(raw) } }
上記のコードからわかるように、Idには空の文字列から初期化されないようにする特別なルールもあります。
転送の安全なデコード
Swiftは、デコード(およびエンコード)列挙の優れたサポートを提供します。 多くの場合、あなたがしなければならないことは、単にDecodableマッチを宣言するだけです。これはコンパイラーによって自動的に合成されます(列挙の生のタイプはStringまたはIntでなければなりません)。
マップ上にすべてのデバイスを表示するシステムを作成するとします。 デバイスには、起動する場所(必須)とシステム(必須)があります。
enum System: String, Decodable { case ios, macos, tvos, watchos } struct Location: Decodable { let latitude: Double let longitude: Double } final class Device: Decodable { let location: Location let system: System }
今、質問があります。 将来さらにシステムが追加された場合はどうなりますか? 製品ソリューションは、これらのデバイスを表示することで構成されますが、何らかの方法でシステムが「不明」であることを示します。 アプリケーションでこれをどのようにシミュレートする必要がありますか?
デフォルトでは、不明な列挙値が検出されると、Swiftは.dataCorruptedエラーをスローします。
{ "location": { "latitude": 37.3317, "longitude": 122.0302 }, "system": "caros" }
do { let device = try JSONDecoder().decode(Device.self, from: json.data(using: .utf8)!) } catch { print(error) // Prints "Cannot initialize System from invalid String value caros" }
システムを安全な方法でモデル化およびデコードするにはどうすればよいですか? 1つの方法は、システムプロパティをオプション、つまり「不明」にすることです。 システムを安全にデコードする最も簡単な方法は、カスタムinit(デコーダー:デコーダーから)スローイニシャライザーを実装することです:
final class Device: Decodable { let location: Location let system: System? init(from decoder: Decoder) throws { let map = try decoder.container(keyedBy: CodingKeys.self) self.location = try map.decode(Location.self, forKey: .location) self.system = try? map.decode(System.self, forKey: .system) } private enum CodingKeys: CodingKey { case location case system } }
このバージョンでは、システムの価値に関して起こりうるすべての問題を単に無視することに注意してください。 これは、「破損した」データ(たとえば、キーシステムの欠落、123、null、空のオブジェクト{}-APIコントラクトに応じて)もnil(「不明」)としてデコードされることを意味します。 「不明な文字列をnilとしてデコードする」と言うより正確な方法:
self.system = System(rawValue: try map.decode(String.self, forKey: .system))
手動デコードはほとんどありません
前の例では、カスタム初期化子init(デコーダー:デコーダーから)スローを実装する必要がありましたが、これは非常に多くのコードであることが判明しました。 幸いなことに、これをより簡潔に行う方法はいくつかあります。
特定のタイプのパラメーターを取り除く
1つのオプションは、明示的な型パラメーターを取り除くことです。
extension KeyedDecodingContainer { public func decode<T: Decodable>(_ key: Key, as type: T.Type = T.self) throws -> T { return try self.decode(T.self, forKey: key) } public func decodeIfPresent<T: Decodable>(_ key: KeyedDecodingContainer.Key) throws -> T? { return try decodeIfPresent(T.self, forKey: key) } }
Postの例に戻り、webURLプロパティで拡張してみましょう(オプション)。 以下に公開されているデータをデコードしようとすると、メインエラーとともに.dataCorruptedエラーが発生します。
struct PatchParameters: Swift.Encodable { let name: Parameter<String>? } func encoded(_ params: PatchParameters) -> String { let data = try! JSONEncoder().encode(params) return String(data: data, encoding: .utf8)! } encoded(PatchParameters(name: nil)) // prints "{}" encoded(PatchParameters(name: .null)) //print "{"name":null}" encoded(PatchParameters(name: .value("Alex"))) //print "{"name":"Alex"}"