LoginSignup
20
13

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-13

はじめに

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

ここでは Currencylayer のAPIを例に考えてみます。

http://apilayer.net/api/live?access_key={アクセスキー}&source={通貨}&format=1
成功時
{
  "success":true,
  "terms":"https:\/\/currencylayer.com\/terms",
  "privacy":"https:\/\/currencylayer.com\/privacy",
  "timestamp":1578845345,
  "source":"USD",
  "quotes":{
    "USDAED":3.673204,
    "USDZWL":322.000001
  }
}
失敗時
{
  "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 の値で分岐させます。

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 さんから伺ったので、備忘録として残します。

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

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

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

…と思ったのですが、 自分の実力ではすぐにはできませんでした
途中まで記述したコードを載せますが、見当違いのことをしているかもしれません。

コメントで教えていただき、やっと改善できました。

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

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

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

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

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

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() の定義は省略しますが、コールバックのクロージャです。

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を使うことで、共通部分をまとめることができました✨

20
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
13