[Swift 4] SwiftyJSONを使わずにシンプルにJSONをデータ構造化する

logo_swift_400x400

はじめに

モバイルアプリサービス部の中安です。

WebAPIなどデータを取得する際にはJSONを使用してデータを受け取ることが多いかと思います。 その際にはSwiftyJSONなどのライブラリを使用してJSONをシリアライズすることで構造体に変換するなどの処理をするかと思います。

しかし、Swift4からは新たに加わったプロトコルCodableを使用すれば、ライブラリを使わずシンプルにそういった処理ができるようになるようです。

サンプル

よくWebAPIのJSONサンプルとして使われるlivedoor天気予報API(大阪)のデータを使用します。 今回は構造化の話だけに絞るので、ネットワーク処理の話は省きます。

このAPIで返ってくるデータのインターフェイスを元に構造体を定義しておきます(一部省略しています)。 イニシャライザも何もないデータの器だけ要しておくだけで十分です。

struct WeatherNews {
    let title: String
    let publicTime: String
    let forecasts: [Forecast]
    let location: WeatherLocation
    let description: WeatherDescription
}

struct Forecast {
    let dateLabel: String
    let telop: String
    let date: String
    let temperature: TemperatureCollection
    let image: WeatherImage
}

struct TemperatureCollection {
    let min: Temperature?
    let max: Temperature?
}

struct Temperature {
    let celsius: String
    let fahrenheit: String
}

struct WeatherImage {
    let width: Int
    let height: Int
    let title: String
    let url: String
}

struct WeatherLocation {
    let city: String
    let area: String
    let prefecture: String
}

struct WeatherDescription {
    let text: String
    let publicTime: String
}

Codableプロトコル

それぞれの構造体をすべてCodableプロトコルに準拠させます。 特に実装しなければならないイニシャライザもメソッドはありません。準拠することを宣言するだけです。

struct WeatherNews: Codable {
    let title: String
    let publicTime: String
    let forecasts: [Forecast]
    let location: WeatherLocation
    let description: WeatherDescription
}

struct Forecast: Codable {
    let dateLabel: String
    let telop: String
    let date: String
    let temperature: TemperatureCollection
    let image: WeatherImage
}

// 以下、他の構造体も同じように": Codable"を付ける

そして、JSONDecoderクラスによってデコードをするだけです。

// jsonString = WebAPIから取ってきた生のJSON文字列とします

let weatherNews = try! JSONDecoder().decode(WeatherNews.self, from: jsonString.data(using: .utf8)!)

たったこれだけで、JSONの内容を元にしたWeatherNewsのオブジェクトが生成されます。 (※ただし、型やオプショナルなどには気をつけてください。少しでも間違えると変数weatherNewsはnilを返してしまいます)

これで、例えば下のようにSwiftyJSONを使ってプロパティひとつひとつに値を入れていく必要もなくなります。

struct WeatherImage {
    let width: Int
    let height: Int
    let title: String
    let url: String
    
    init(json: JSON) {
        width = json["width"].intValue
        height = json["height"].intValue
        title = json["title"].stringValue
        url = json["url"].stringValue
    }
}

楽になりますね。

ちなみにデータを作ったあとは、こんなふうに処理をすると天気予報っぽくなりました。

print(weatherNews.title)
weatherNews.forecasts.forEach { forecast in
    print("\(forecast.dateLabel)(\(forecast.date)) => \(forecast.telop)")
}
print(weatherNews.description.text)
大阪府 大阪 の天気
今日(2017-08-09) => 晴のち曇
明日(2017-08-10) => 曇り
明後日(2017-08-11) => 曇時々晴
 近畿地方は、気圧の谷の影響により曇りの所もありますが、おおむね晴れ
ています。

 今夜の近畿地方は、山陰沖の前線の影響により、おおむね曇りで雨や雷雨
の所があるでしょう。

 明日の近畿地方は、山陰沖の前線や低気圧の影響により、おおむね曇りで
夕方から夜のはじめ頃にかけ雨が降り、雷を伴い激しく降る所がある見込み
です。
 大阪府では、明日は高温が予想され、熱中症の危険が特に高くなる見込
みです。暑さを避け、水分をこまめに補給するなど、十分な対策をとってく
ださい。

JSONへの変換

Codableに準拠したオブジェクトは、JSONEncoderクラスによって逆にJSON文字列に変換することもできます。

// weatherNews = 前項で作ったWeatherNewsオブジェクトとします

let data = try! JSONEncoder().encode(weatherNews)
let json = String(data: data, encoding: .utf8)!
print(json)

これを実行すると、JSON文字列が出力されるはずです。

JSONでテストデータを作ったり、JSONをパラメータにしなければならないAPI仕様などの場合に使えますね。

任意の変換をさせる場合

Codableプロトコルの定義は、Decodable & Encodableとされています。 デコードする際にはDecodableのイニシャライザが走ることになります。 ですので、このイニシャライザを定義することによって任意の型などに変換することができます。

その際に使用するのはCodingKeyというプロトコルです。これをString型の列挙子と組み合わせて以下のように実装します。

// 先程までの定義
struct Temperature: Codable {
    let celsius: String
    let fahrenheit: String
}
// 摂氏はIntに華氏はDoubleで持っておきたい・・・となった場合の例
struct Temperature: Codable {
    let celsius: Int
    let fahrenheit: Double
    
    enum Key: String, CodingKey {
        case celsius, fahrenheit
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        // 一旦文字列として取得
        let celsiusString    = try container.decode(String.self, forKey: .celsius)
        let fahrenheitString = try container.decode(String.self, forKey: .fahrenheit)
        // 文字列を各々の型に変換
        celsius    = Int(celsiusString) ?? 0
        fahrenheit = atof(fahrenheitString)
    }
}

celsiusfahrenheitもJSON上は文字列型なので、文字列以外でデコードするとエラーになってしまいます。 なので、イニシャライザを使って文字列で取ったものを型変換してメンバ変数に入れる流れにしています。

まとめ

この仕組みを使うと、構造体がずいぶんとすっきりになるような気がします。

Alamofireなどと相性よくコード量が減るSwiftyJSONですが、さらにコードをダイエットさせられるかもしれません。