your3i’s blog

iOSエンジニア。頑張る⚉

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キーを使う必要がない!の環境で使うと便利かも。笑。