レスポンスのJSONが異なるAPIにオプショナルを使わず対応する方法(Swift)

## はじめに

成功時と失敗時で、レスポンスのJSONの構造が異なるAPIはよくあると思います。

ここでは [Currencylayer](https://currencylayer.com/) のAPIを例に考えてみます。

```
http://apilayer.net/api/live?access_key={アクセスキー}&source={通貨}&format=1
```

```json:成功時
{
  "success":true,
  "terms":"https:\/\/currencylayer.com\/terms",
  "privacy":"https:\/\/currencylayer.com\/privacy",
  "timestamp":1578845345,
  "source":"USD",
  "quotes":{
    "USDAED":3.673204,
    "USDZWL":322.000001
  }
}
```

```json:失敗時
{
  "success":false,
  "error":{
    "code":105,
    "info":"Access Restricted - Your current Subscription Plan does not support Source Currency Switching."
  }
}
```

対応方法はいくつかあると思いますが、私はどちらか片方にのみ存在するキーをオプショナル型にする方法を考えました。
（使わないキーは省略しています）

```ExchangeRatesDTO.swift
struct ExchangeRatesDTO: Decodable {
    // 共通
    let success: Bool // オプショナル型にしなくていい

    // 成功時のみ
    let source: String?
    let quotes: [String: Double]?

    // 失敗時のみ
    let error: CurrencylayerError?

    struct CurrencylayerError: Decodable {
        let code: Int
        let info: String
    }
}
```

デコード時は `success` の値で分岐させます。

```swift:CurrenylayerProvider.swift
let data = #"""
    {
      "success":true,
      "terms":"https:\/\/currencylayer.com\/terms",
      "privacy":"https:\/\/currencylayer.com\/privacy",
      "timestamp":1578672546,
      "source":"USD",
      "quotes":{
        "USDJPY":109.541505,
        "USDUSD":1,
        "USDZWL":322.000001
      }
    }
    """#.data(using: .utf8)!

do {
    let dto = try JSONDecoder().decode(ExchangeRatesDTO.self, from: data)
    if !dto.success {
        fatalError()
    }
    // 成功時の処理をここに記述する
    } catch {
        fatalError("error: \(error)")
    }
}
```

しかし、これだと以下のデメリットがあります。

- オプショナル型なので呼び出すたびにアンラップが必要になる
- 成功時と失敗時でJSONの構造がコードから読み取れない
  - コメントでわかりやすくすることはできる

列挙型を使ってデメリットを解決する方法を [@takasek](https://twitter.com/takasek) さんから伺ったので、備忘録として残します。

## 列挙型を使ってオプショナルを消す

大変ありがたいことに、Twitterで教えていただきました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">こういうかんじですー <a href="https://t.co/LFy4BXuaMI">pic.twitter.com/LFy4BXuaMI</a></p>&mdash; takasek (@takasek) <a href="https://twitter.com/takasek/status/1216265329707380737?ref_src=twsrc%5Etfw">January 12, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こちらを元に、上記のコードを改善します。

~~…と思ったのですが、 __自分の実力ではすぐにはできませんでした__ 。
途中まで記述したコードを載せますが、見当違いのことをしているかもしれません。~~
[コメント](https://qiita.com/uhooi/items/5fefd61592eecc030844#comment-7c2cfaf9b78b074eec63)で教えていただき、やっと改善できました。

まず、成功時のレスポンスを定義します。
失敗時にはデコードされないため、オプショナル型を外すことができます。

```swift:ExchangeRateDTO.swift
struct ExchangeRateDTO: Decodable {
    let source: String // `?` を外せる
    let quotes: [String: Double] // `?` を外せる
}
```

次に、失敗時のレスポンスを定義します。
今回のAPIではエラーのレスポンスが共通のため、構造体の入れ子から出しました。

```swift:CurrencylayerAPIError.swift
struct CurrencylayerAPIError: Error, Decodable {
    let code: Int
    let info: String
}
```

最後に、今回のAPIをデコードする共通の列挙型を定義します。
成功時のレスポンスの型を外から注入することで、他のAPIもこちらの列挙型でデコードできます。

```swift:CurrencylayerAPIResult.swift
enum CurrencylayerAPIResult<T: Decodable>: Decodable {
    case success(T)
    case failure(CurrencylayerAPIError)

    private enum CodingKeys: String, CodingKey {
        case success
        case error
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if try container.decode(Bool.self, forKey: .success) {
            self = .success(try T(from: decoder))
        } else {
            self = .failure(try container.decode(CurrencylayerAPIError.self, forKey: .error))
        }
    }
}
```

呼び出し方は以下の通りです。
`success()` と `failure()` の定義は省略しますが、コールバックのクロージャです。

```swift:CurrenylayerProvider.swift
let data = #"""
    {
      "success":true,
      "terms":"https:\/\/currencylayer.com\/terms",
      "privacy":"https:\/\/currencylayer.com\/privacy",
      "timestamp":1578672546,
      "source":"USD",
      "quotes":{
        "USDJPY":109.541505,
        "USDUSD":1,
        "USDZWL":322.000001
      }
    }
    """#.data(using: .utf8)!

do {
    let response = try JSONDecoder().decode(CurrencylayerAPIResult<ExchangeRateDTO>.self, from: data)
    switch response {
    case .success(let exchangeRate):
        success(exchangeRate)
    case .failure(let error):
        failure(error)
    }
} catch {
    fatalError("error: \(error)")
}
```

## おわりに

オプショナルを使わず、成功時と失敗時でどのようなレスポンスが返るかコードから読み取れるようになりました！
さらにGenericsを使うことで、共通部分をまとめることができました✨