Swift の JSONDecoder で keyDecodingStrategy を使う時の注意点

# まとめ

- `JSONDecoder.keyDecodingStrategy` が `convertFromSnakeCase` の場合、 `Decodable` を適用した struct で `CodingKeys` を指定する際は key 名の指定に気をつける
- key 名はすでに camelCase に変換した状態で指定する

# JSONDecoder と Decodable

Swift で JSON を struct に変換できる [`JSONDecoder`](https://developer.apple.com/documentation/foundation/jsondecoder) 、便利ですよね。

```swift
let decoder = JSONDecoder()
try decoder.decode(Response.self, from: data)
```

などとして、JSON 形式であるはずの `Data` 型 `data` を `Response` 型に変換できます。

基本的に構造体のプロパティ名と JSON の Key 名が揃っているならば、 `Response` 型が単に `Decodable` を適用していればなんの問題もありません。

```swift
let jsonData = """
{
  "id": 123,
  "screenName": "mokumoku"
}
""".data(using: .utf8)!

struct Response: Decodable {
  let id: Int
  let screenName: String
}

let decoder = JSONDecoder()
try decoder.decode(Response.self, from: jsonData)  // 成功
```

# CodingKeys

しかしそう上手くいくことは少なく、 Swift は lowerCamelCase 文化であるのに対し API の key は snake_case で設定されていることが多いです。
そのような時は `Response` 型に `CodingKeys` を設定します。

```swift
let jsonData = """
{
  "id": 123,
  "screen_name": "mokumoku"
}
""".data(using: .utf8)!


struct Response: Decodable {
  let id: Int
  let screenName: String

  private enum CodingKeys: String, CodingKey {
    case id
    case screenName = "screen_name"
  }
}

let decoder = JSONDecoder()
try decoder.decode(Response.self, from: jsonData)  // 成功
```

これだとどんな key 名にも対応できてよいのですが、例えばもっとたくさんプロパティがあって、一つだけ snake_case な場合は面倒です。
`CodingKeys` を実装する場合はすべてのプロパティを `case` に書き出さないとエラーになるので単純に面倒なのと実装漏れの恐れがあります。

# KeyDecodingStorategy

そんな場合に便利なのが [`KeyDecodingStorategy`](https://developer.apple.com/documentation/foundation/jsondecoder/keydecodingstrategy) です。

- `convertFromSnakeCase`
- `useDefaultKeys`
- `custom(@escaping ([CodingKey]) -> CodingKey)`

の三種類があります。

ここで `convertFromSnakeCase` を指定すると、自動的に JSON の key 名の snake_case を lowerCamelCase に変換してくれます。

```swift
let jsonData = """
{
  "id": 123,
  "screen_name": "mokumoku"
}
""".data(using: .utf8)!


struct Response: Decodable {
  let id: Int
  let screenName: String
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase  // ここが大事
try decoder.decode(Response.self, from: jsonData)  // CodingKeys がなくても成功
```

便利ですね。
しかし、ここで JSON の key 名が微妙なために `CodingKeys` を実装しようとするとハマるポイントがあったので紹介します。（前フリが長すぎる）

# KeyDecodingStrategy と CodingKeys 併用時の注意点

[Twitter の API レスポンス](https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object.html) はそのサービスの特性上、 `id` が `UInt64` に収まらないものがあるため使い物になりません。
ではどうするかというと、 `id_str` という key に `String` で正しい値が返ってきているのでそれを利用します。

Swift 側では `id: String` として、これに `id_str` を対応させるようにします。

これをそのままやると以下のようになりますが、decode に失敗します。

```swift

let jsonData = """
{
  "id": 123,
  "id_str": "123",
  "screen_name": "mokumoku"
}
""".data(using: .utf8)!


struct Response: Decodable {
    let id: String
    let screenName: String

    private enum CodingKeys: String, CodingKey {
        case id = "id_str"
        case screenName
    }
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
try decoder.decode(Response.self, from: jsonData)  // "id_str" という key がないですというエラー
```

ではどうするかというと、`enum` の `rawValue` を指定するところを lowerCamelCase にしてから指定します。

```swift

private enum CodingKeys: String, CodingKey {
  case id = "idStr"
  case screenName
}
```

どうやら JSON の key 名を keyDecodingStrategy に従って変換してから、struct の CodingKeys にマッチさせているようで、ちょっと気持ち悪いのですがこうすると上手くいきました。

# まとめ

- `keyDecodingStrategy` と `CodingKeys` を併用する場合は key 名の指定に気をつけよう

間違っていたり、もっとベストプラクティスがあるかもしれないのでもしご存知ならばコメントなどで教えていただけるとありがたいです。

## リンク

- https://developer.apple.com/documentation/foundation/jsondecoder
- https://developer.apple.com/documentation/foundation/jsondecoder/keydecodingstrategy