JSONDecoderで.convertFromSnakeCase指定時の挙動一覧

Swiftにおいて，**JSONDecoder.keyDecodingStrategy**に**.convertFromSnakeCase**を指定したときの挙動について，一部混乱したものがあったので，メモ．

# 環境
* Swift: 4.1.2


# JSONDecoder.keyDecodingStrategyについて
**(既にご存知の方は読み飛ばしてください)**


例えば，サーバからのレスポンスが次のようなJSONだったとします．

```json
{
    "person_name": "kagemiku",
    "person_age": 23
}
```

このJSONを素直にパースしようすると，**Codable Protocol**に適合させた次のような構造体を用意する必要があります．

```swift
struct Person: Codable {
    let person_name: String
    let person_age: Int
}
```

構造体のフィールド名がSwiftのコーディングスタイル(camelcase)になっておらず少しキモいです．構造体内に**CodingKeys**を定義することによって解決することはできますが，フィールドの数が多くなってくると，書くのも修正するのも大変めんどいです．

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int

    enum CodingKeys: String, CodingKey {
        case personName = "person_name"
        case personAge = "person_age"
    }
}
```

「一つのJSONオブジェクト内で，keyがキャメルケースだったり，スネークケースだったりする」という悲しいレスポンスが帰ってくる場合は，上記の手法しかとりようがないのですが，「全部スネークケースである」ことが決まっている場合，次のように，**JSONDecoder**に対してオプションを与えてやることで，上記のような冗長気味な記述をせずに済みます．

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}

let jsonData = """
{
    "person_name": "kagemiku",
    "person_age": 23
}
""".data(using: .utf8)!

let jsonDecoder = JSONDecoder()
// ↓↓↓↓↓↓↓↓
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
// ↑↑↑↑↑↑↑↑

do {
    let result = try jsonDecoder.decode(Person.self, from: jsonData)
    print(result)
} catch {
    print(error.localizedDescription)
}
```


この`jsonDecoder.keyDecodingStrategy`に与えている`.convertFromSnakeCase`というオプションは，[公式Doc](https://developer.apple.com/documentation/foundation/jsondecoder/keydecodingstrategy/convertfromsnakecase)によると，JSONのキーに対して次のような変換を施した上で，構造体フィールドとのマッピングを行ってくれます


> 1. Capitalize each word that follows an underscore.
>   (アンダースコアに続く単語を大文字始まりにする)
> 
> 2. Remove all underscores that aren't at the very start or end of the string.
>   (初めと終わり以外にあるアンダースコアを全て取り除く)
> 
> 3. Combine the words into a single string.
>   (単語を一つの文字列へと連結する)

ぱっと読んで分かるようなわからないような．．．
なので，こちらの挙動をまとめてみました．

# 挙動一覧
## 1. JSON keyが通常のsnakecaseの場合

```json
{
    "person_name": "kagemiku",
    "person_age": 23
}
```

次の構造体を用意することでマッピングできます

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}
```

## 2. JSON keyが大文字のsnakecaseの場合

```json
{
    "PERSON_NAME": "kagemiku",
    "PERSON_AGE": 23
}
```

1. と同じくこちらでOK

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}
```

## 3. JSON keyの途中で大文字だったり小文字だったりする場合

```json
{
    "peRson_nAme": "kagemiku",
    "pERSON_aGE": 23
}
```

こちらも1. と同じ構造体でOKなようです．

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}

```

一度JSON Keyは小文字にならされている？🤔


## 4. JSON keyの先頭，もしくは末尾に複数のアンダースコアがある場合

```json
{
    "__person_name": "kagemiku",
    "person_age___": 23
}
```

先頭，もしくは末尾にあるアンダースコアの連続は，全てがそのまま残るようです

```swift
struct Person: Codable {
    let __personName: String // アンダースコア2個
    let personAge___: Int    // アンダースコア3個
}
```

## 5. JSON keyがcamelcaseの場合

```json
{
    "personName": "kagemiku",
    "personAge": 23
}
```

**.convertFromSnakeCase**を指定している場合に，アンダースコアで結ばれていない，すなわちcamelcaseのkeyが与えられた場合，全て小文字にならされることはなく，そのまま使われるようです．

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}
```

もう少し詳しくやると，次のようなJSONのとき，

```json
{
    "PERSON_NAME": "kagemiku",
    "personAge": 23
}
```

こうなります．

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}
```

**PERSON_NAME**は全て大文字のsnakecaseなので，2. 3. と同様に一度小文字にならされているっぽいです．一方，**personAge**の方はアンダースコアのない，camelcaseとなっているので，**PERSON_NAME**のように全て小文字にならされることはなく，そのままとなっています．謎い😇

もっとやると，次のようなJSONのとき，

```json
{
    "person_name": "kagemiku",
    "_personAge_": 23
}
```

相変わらず`_personAge_`はそのままとなります．

```swift
struct Person: Codable {
    let personName: String
    let _personAge_: Int
}
```

**単語の途中に**アンダースコアがある場合のみ，はじめに説明した変換が施される？？🤔🤔🤔


## 6. JSON Keyの途中に連続したアンダースコアがある

```json
{
    "person__name": "kagemiku",
    "person___age": 23
}
```

途中にある連続したアンダースコアもきちんと全て取り除かれるようです．

```swift
struct Person: Codable {
    let personName: String
    let personAge: Int
}
```

## 7. JSON Keyにめっちゃアンダースコアがある

```json
{
    "person_n_a_m_e": "kagemiku",
    "person_a_g_e": 23
}
```

これは仕様どおり．

```swift
struct Person: Codable {
    let personNAME: String
    let personAGE: Int
}
```

# まとめ

1. **単語の途中に**アンダースコアが含まれている場合，2. に進む．そうでないなら何もせず，そのままkeyとなる
2. 一旦全て小文字にならす(?)
3. アンダースコアに続く単語を大文字始まりにする
4. keyの途中にあるアンダースコアを**全て**取り除く
5. 単語を一つの文字列へと連結する

この辺の挙動詳しい方いましたら，ぜひコメント or 編集リクエストお願いします🙏🙏🙏

# 参考サイト
* [JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase - Apple Developer Documentation](https://developer.apple.com/documentation/foundation/jsondecoder/keydecodingstrategy/convertfromsnakecase)