はじめに
成功時と失敗時で、レスポンスの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."
}
}
対応方法はいくつかあると思いますが、私はどちらか片方にのみ存在するキーをオプショナル型にする方法を考えました。
(使わないキーは省略しています)
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
の値で分岐させます。
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で教えていただきました。
こういうかんじですー pic.twitter.com/LFy4BXuaMI
— takasek (@takasek) January 12, 2020
こちらを元に、上記のコードを改善します。
…と思ったのですが、 自分の実力ではすぐにはできませんでした 。
途中まで記述したコードを載せますが、見当違いのことをしているかもしれません。
コメントで教えていただき、やっと改善できました。
まず、成功時のレスポンスを定義します。
失敗時にはデコードされないため、オプショナル型を外すことができます。
struct ExchangeRateDTO: Decodable {
let source: String // `?` を外せる
let quotes: [String: Double] // `?` を外せる
}
次に、失敗時のレスポンスを定義します。
今回のAPIではエラーのレスポンスが共通のため、構造体の入れ子から出しました。
struct CurrencylayerAPIError: Error, Decodable {
let code: Int
let info: String
}
最後に、今回のAPIをデコードする共通の列挙型を定義します。
成功時のレスポンスの型を外から注入することで、他のAPIもこちらの列挙型でデコードできます。
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()
の定義は省略しますが、コールバックのクロージャです。
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を使うことで、共通部分をまとめることができました✨