DecodableのDecodeを簡潔に書きたい
Swift4でDecodableつかってますか〜?
公式にサポートしてもらえると本当にありがたいですよね。
しかしながら汎用的なパースをしようとすると若干、コードが冗長になります
公式ドキュメントによると、カスタムなデコードの戦略を図る場合は下記のようにイニシャライザとデコードを実装する必要があります
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) } }
冗長ですね。値がnilや予期しない値だった場合のハンドリングが考慮されていないのでバギーです
これらを簡潔に書く軽いExtensionを作ってみたので、例をもとに説明してみたいと思います。
目指した要件は2点です
- パースが失敗したときのデフォルト値を設定する
- String以外で表現できるプリミティブな型がStringで渡ってくる場合のフォールバック
1. パースが失敗したときのデフォルト値を設定する
犬が受けたワクチンをリストするAPIのJSONのレスポンスを例にしてみます。
Dog.json
[ { "name": "Spring", "vaccinations": [ { "name": "Rabies" }, { "name": "Corona" } ] } ]
構造体
Dog.swift
// 予防接種の種類 struct Vaccination: Decodable { let name: String } struct Dog: Decodable { let name: String let vaccinations: [Vaccination] }
例えば、予防接種を受けた数を表現するとき、受けたことがなかったら空の配列を作りたいとします。
しかし、空の配列は返さず、nullもしくはそもそもそのKey&Valueを返さないAPIなどもありえます。
Dog.json
[ { "name": "Spring", "vaccinations": null } ]
この場合、デコードエラーになるのでパースエラーのときにはvaccinationsに空配列をセットする実装を追加しましょう
Dog.swift
struct Dog: Decodable { let name: String let vaccinations: [Vaccination] init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decode(String.self, forKey: .name) do { vaccinations = try values.decode([Vaccination].self, forKey: .vaccinations) } catch { vaccinations = [] } } private enum CodingKeys: String, CodingKey { case name case vaccinations } }
問題点
do - try - catch
が冗長- パラメータが増える度に
do - try - catch
を実装する必要がある - decodeの型を明示的に指定する必要がある
ということで
失敗時のデフォルト値を指定しつつ、型推論でよしなに型を指定してくれる実装をしたいと思います。
KeyedDecodingContainerを拡張する
decoder.container(keyedBy: CodingKeys.self)
で取得できる型の拡張を書くのが手っ取り早いです
KeyedDecodingContainer+Helper.swift
extension KeyedDecodingContainer { func decode<ResultType: Decodable>(forKey key: Key, defaultValue: ResultType? = nil) -> ResultType? { do { return try decode(ResultType.self, forKey: key) } catch let error { NSLog(error) return defaultValue } } }
これで5行が1行になりました
Dog.swift
struct Dog: Decodable { let name: String let vaccinations: [Vaccination] init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decode(String.self, forKey: .name) vaccinations = values.decode(forKey: .vaccinations) ?? [] } private enum CodingKeys: String, CodingKey { case name case vaccinations } }
2. String以外で表現できるプリミティブな型がStringで渡ってくる場合のフォールバック
次に犬の名前と年をパースする例を上げてみます。
Dog.json
[ { "name": "Spring", "age": "10" } ]
Dog.swift
struct Dog: Decodable { let name: String let age: Int init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decode(String.self, forKey: .name) age = values.decode(forKey: .age) ?? 0 } }
age
がこれでは0になってしまいます。
ここで
- StringからIntにパースする
- それでも失敗したらデフォルト値にする
という要件をKeyedDecodingContainerのextensionで実装してみます
KeyedDecodingContainer+Helper.swift
extension KeyedDecodingContainer { func decodeWithString<ResultType: FallbackableResultType>(forKey key: Key, defaultValue: ResultType? = nil) -> ResultType? { do { return try decode(ResultType.self, forKey: key) } catch let error { NSLog(error) do { let string = try decode(String.self, forKey: key) return ResultType.toResultType(string: string, defaultValue: defaultValue) } catch let error { NSLog(error) return defaultValue } } } } internal protocol FallbackableType { static func toResultType(string: String, defaultValue: Self?) -> Self? } typealias FallbackableResultType = FallbackableType & Decodable extension Int: FallbackableType { internal static func toResultType(string: String, defaultValue: Int?) -> Int? { return Int(string) ?? defaultValue } }
これで、無事、デコードできるようになりました。
肝としては2点
- 最初に各型にデコードできるかトライ
age: 10
だった場合でも処理できるのでAPIを修正した場合にバグにならない
- FallbackableTypeを用意して、String→各型への変換処理を移譲している
- FallbackableTypeに準拠した型に制限していますので、Stringから該当する型に変換する戦略を書く必要があります。
まとめ
ということで、KeyedDecodingContainer
を拡張すればデコード処理は簡潔に書ける〜という話でした