【Swift】JSON内にある様々なIDを型安全に扱えるようDecodableでdecodeする

この記事は[ニフティグループ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nifty)の13日目の記事です。

# TL;DR
iOSアプリではWEB APIやローカルのファイル等、外部から情報を取得して画面に表示することが多いです。
それらの情報の中には多くのIDが含まれています。
IDをキーにしてローカルにキャッシュしたり、IDをキーにして検索を実行したり……
さまざまな場面で利用されているIDを型安全に扱う方法として、IDをstructで表現する方法が有名だと思います。
本記事では以下を紹介します。

* IDを型安全に扱う方法のご紹介
* JSON内のIDを型安全に扱えるようdecodeする方法

# IDを型安全に扱う方法
GitHubのAPIを参考にご説明します。※説明の都合上、簡略化しています。
以下のようなリポジトリ検索のリクエストを送信した場合のレスポンスはこちらです。
`https://api.github.com/search/repositories?q=swift`

```json
{
  "total_count": 127415,
  "incomplete_results": false,
  "items": [
    {
      "id": 44838949,  # リポジトリのID
      "full_name": "apple/swift",
      "owner": {
        "login": "apple",
        "id": 10639145  # ユーザのID
      }
    },
    {
      "id": 27398751,
      "full_name": "btrn/Swift",
      "owner": {
        "login": "btrn",
        "id": 8502419
      }
    }
  ]
}
```

このレスポンスの中にIDが2種類以上存在することに気づきましたか?
それぞれ、リポジトリのIDとユーザのIDで、意味が異なるIDです。
こちらをそのままJSONに書かれている通りにSwiftの実装におこすと以下のようになります。

```swift:Repository.swift
struct Repository {
    let id: Int
    let fullName: String
    let owner: User
}
```

```swift:User.swift
struct User {
    let id: Int
    let login: String
}
```

愚直に実装するとどちらのIDも`Int`で表現することになると思います。
ですが、以下のような場合にバグになってしまう可能性があります。

| 項目            | 情報                                              |
| -------------- | ------------------------------------------------- |
| 実装したい機能    | リポジトリのお気に入り                            |
| 必要な情報       | リポジトリのID（`Int`）                           |
| ミス            | ユーザのID（`Int`）を保存してしまった             |
| 原因            | 同じ `Int` を扱うのでコンパイル時に気づけなかった |

そこで以下のようにIDをstructで表現することによって型安全にすることができます。

```swift:Repository.swift
struct Repository {
    let id: ID
    let fullName: String
    let owner: User

    struct ID {
        let rawValue: Int
    }
}
```

```swift:User.swift
struct User: Decodable {
    let id: ID
    let login: String

    struct ID {
        let rawValue: Int
    }
}
```

それぞれ`Repository.ID`型と`User.ID`型の別々の型になり、**間違って実装したらコンパイルエラーが発生するようになりました!!💪**

```swift
// IDをIntで扱うとリポジトリのIDでもユーザのIDでも、どちらでも使える
func save(from id: Int)

// リポジトリのID以外は受け取れない
func save(from id: Repository.ID)
```

# JSON内のIDを型安全に扱えるようdecodeする方法
ここまででIDを型安全に扱う方法をご紹介してきました。
では、実際にGitHubのAPIのレスポンスを`Decodable`プロトコルを利用してそれぞれの型に落としていくことを考えたいと思います。

## `Decodable`プロトコルに準拠
```swift:Repository.swift
struct Repository: Decodable {
    ...
    struct ID: Decodable {
        let rawValue: Int
    }
}
```
```swift:User.swift
struct User: Decodable {
    ...
    struct ID: Decodable {
        let rawValue: Int
    }
}
```
コンパイルエラーはでなくなりますが、structで型のネストをしてしまったので、実行時に落ちてしまいます。
現状は以下のようなレスポンスを期待していると解釈されてしまいます。

```json:期待しているレスポンス(抜粋)
    {
      "id": {  # 型のネスト
        "rawValue": 44838949  # リポジトリのID
      },
      "full_name": "apple/swift",
      "owner": {
        "login": "apple",
        "id": {  # 型のネスト
          "rawValue": 10639145  # ユーザのID
        }
      }
    }
```

## 型のネストを解決
これを先程のレスポンスをデコードできるようにしつつ、型安全を保つには
`RawRepresentable`プロトコルに準拠させます
https://developer.apple.com/documentation/swift/rawrepresentable
enumがIntやStringを継承すると暗黙的に`rawValue`というプロパティが出現するのはこのプロトコルのおかげです。

最終的なコードはこのようになります。

```swift:Repository.swift
struct Repository: Decodable {
    let id: ID
    let fullName: String
    let owner: User

    struct ID: RawRepresentable, Decodable {
        let rawValue: Int
    }
}
```
```swift:User.swift
struct User: Decodable {
    let id: ID
    let login: String

    struct ID: RawRepresentable, Decodable {
        let rawValue: Int
    }
}
```
```swift
let data = """
[
  {
    "id": 44838949,
    "full_name": "apple/swift",
    "owner": {
      "login": "apple",
      "id": 10639145
    }
  },
  {
    "id": 27398751,
    "full_name": "btrn/Swift",
    "owner": {
      "login": "btrn",
      "id": 8502419
    }
  }
]
""".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let repositories = try! decoder.decode([Repository].self, from: data)
```

# まとめ
* iOSアプリではアプリ外からも多くの情報を取得して表示している
* それらの情報にはさまざまなIDが含まれている
* IDは取り間違えるとバグになるため、structを用いて別々の型にすることで、コンパイラによるバグの検出が可能
* `Decodable`プロトコルを用いてstruct化したIDをdecodeするにはIDの型を`RawRepresentable`プロトコルに準拠させる

# 最後に
明日は@5_21maimaiさんが「5年もののiOSアプリで、ついに自動テストを開始した話」をしてくれます!お楽しみに!!