Help us understand the problem. What is going on with this article?

Codableでレスポンス形式のゆらぎを吸収するPrice型をつくる

はじめに

アプリで利用するAPIの種類が増えてくると、APIによってAPIレスポンスの形式が異なるという課題が発生します。

例えばこのような価格を返す複数のAPIがあったとします。

Int型で返す
{ "price": 10000 }
String型(価格フォーマットなし)で返す
{ "price": "10000" }
String型(価格フォーマットあり)で返す
{ "price": "10,000円" }

特に複数APIのマッシュアップをするような大規模サービスでは、ドメイン毎にコード規約やレスポンス形式の考え方が異なるため、同じ価格を表現するレスポンスでもこのようなことが発生しがちです。これらのレスポンス形式のゆらぎを吸収するPrice型をCodableでつくってみます。

Price型をつくる

上記の課題を解決するために、以下のようなPrice型を定義してみます。
Price型はDecodableに準拠し、init(from decoder: Decoder) throwsメソッドを実装することでデコード処理をカスタマイズしています。

Price型の実装
struct Price: RawRepresentable, Decodable {
    typealias RawValue = Int

    let rawValue: Int

    init?(rawValue: Self.RawValue) {
        self.rawValue = rawValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        // String型からInt型に変換する
        // 後述するextractNumber関数を用いて文字列から数字のみを取り出す
        if let stringValue = try? container.decode(String.self), let intValue = Int(stringValue.extractNumber) {
            self.rawValue = intValue
            return
        }

        let intValue = try container.decode(Int.self)
        self.rawValue = intValue
    }
}

文字列から数字のみを取り出すextractNumber関数は以下のように実装しています。

文字列から数字のみを取り出すextractNumber関数
extension String {
    var extractNumber: String {
        return trimmingCharacters(in: CharacterSet.decimalDigits.inverted)
            .replacingOccurrences(of: ",", with: "")
    }
}

Price型を利用してデコードする

以下のようなJSONレスポンスを想定します。

JSONレスポンス
{
    "price1": 1000,
    "price2": "2000",
    "price3": "3,000円",
    "price4": "¥4,000"
}

以下のように、レスポンスの価格の表現形式によらず、Price型としてデコードができます。

レスポンスの価格の表現形式によらず、Price型としてデコード
let jsonString = """
{
    "price1": 1000,
    "price2": "2000",
    "price3": "3,000",
    "price4": "¥4,000"
}
"""
let data = jsonString.data(using: .utf8)!

// レスポンスの価格の表現形式によらず、Price型としてデコード
struct Item: Decodable {
    let price1: Price
    let price2: Price
    let price3: Price
    let price4: Price
}

do {
    let item = try JSONDecoder().decode(Item.self, from: data)
    print(item)
} catch {
    print(error)
}

UI表示のためにさらに便利にする

せっかくPrice型という独自型を定義したので、価格表示のための文字列を返すロジックを実装します。ここでは、価格は3桁区切りのカンマをつけ、〜円の形式で表示するというドメインロジックがあると想定します。

Int型を拡張子し、3桁区切りのカンマを付与した文字列を返すwithCommaプロパティを実装します。それを用いて、Price型にformattedStringプロパティを実装します。

extension Int {
    /// 3桁区切りのカンマを付与した文字列を返す
    var withComma: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return formatter.string(from: NSNumber(value: self)) ?? "\(self)"
    }
}

struct Price: RawRepresentable, Decodable {
    /// 3桁区切りのカンマをつけ、〜円の形式にした文字列
    var formattedString: String {
        return rawValue.withComma + "円"
    }

    // 〜中略〜
}

formattedStringを用いることで、以下のように価格表示のフォーマットが簡単にできます。

print(item.price1.formattedString) // "1,000円"
print(item.price2.formattedString) // "2,000円"
print(item.price3.formattedString) // "3,000円"
print(item.price4.formattedString) // "4,000円"
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away