CodingKeyとKeyDecodingStrategyのconvertFromSnakeCase一緒に使う
概要
iOS9からAPIのリスポンスとアプリモデルの間のマッピング用のCodable(Decodable + Encodable = Codable)が使えるようになった。CodingKeyはdecodeとencodeに必要なキーを定義するとき使うプロトコルである。そして、convertFromSnakeCaseはdecodeするとき、リスポンスのデータのキーをsnake-caseからcamel-caseに変換してくれるツールみたいなもの。
両方一緒に使ってみたとき、よくわからないがこけるケースがあった。記録。
間違ってる例
GroceryProductのproductIDとproductNameを以下のようにjsonからdecodeする。convertFromSnakeCaseがいい感じにやってくれると思ったが…
let data = """ { "product_id": "777", "product_name": "Banana" } """.data(using: .utf8)! struct GroceryProduct: Decodable { var productID: String var productName: String } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try decoder.decode(GroceryProduct.self, from: data) print(result)
こんなエラーが起きた:
No value associated with key CodingKeys(stringValue: \"productID\", intValue: nil) (\"productID\"), converted to product_id.
二つの間違ってるところ
問題1 convertFromSnakeCaseはproduct_idをproductIdに変換するのだ
ドキュメントを読んだら、こう書いてあった↙︎
Note
The JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase strategy can't infer capitalization for acronyms or initialisms such as WYSIWYG or URI.
例えば、base_uri → Converts to: baseUri
だから、product_id → Converts to: productId
修正
そういう訳か。じゃproductIDのsnack_case変換を諦めて、Customキーを指定すればいいだろう?と思って、CodingKeyを使ってGroceryProductをこう修正した。
struct GroceryProduct: Decodable { var productID: String var productName: String private enum CodingKeys: String, CodingKey { case productID = "product_id" // キーを指定 case productName } }
またエラー!!!
No value associated with key CodingKeys(stringValue: \"product_id\", intValue: nil) (\"product_id\").
このエラーは本当に意味わからなかった…
問題2 convertFromSnakeCaseを本当に理解した?
そのdeocdeの順番は:
① convertFromSnakeCaseが使われる → JSONのキーをsnake-caseからcamel-caseに変換
② CodingKeyが使われる → 指定されたキーでそれぞれの値を取る
そのため、CodingKeyが使われる時点でJSONのproduct_idはもうproductIdになった。
修正
let data = """ { "product_id": "777", "product_name": "Banana" } """.data(using: .utf8)! struct GroceryProduct: Decodable { var productID: String var productName: String private enum CodingKeys: String, CodingKey { case productID = "productId" // キーを指定 case productName } } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let result = try decoder.decode(GroceryProduct.self, from: data) print(result)
ようやく直った😭
Output:
GroceryProduct(productID: "777", productName: "Banana")
まとめ
注意
CodingKeyとconvertFromSnakeCaseを一緒に使うとき、CodingKeyでdecodeするときキーはもうcamel-caseに変換されたことに注意。
感想
convertFromSnakeCaseを設定した上で、Customキーを指定することもあるあるだろう。しかし、Customキーを指定するとき、JSONのキーだけじゃなく、convertFromSnakeCaseの変換も考慮する必要があり、謎の曲がりがあってわかりづらい。二つの実装が離れてる場合、バグが生みそう。
だから一緒に使わない方がいいと思った。
どっちかというと、CodingKeyが自由度高いし、単純でわかりやすい。convertFromSnakeCaseはサーバー側とキーの約束をしっかりして、Customキーを使う必要がない!の環境で使うと便利かも。笑。