[Swift 4] SwiftyJSONを使わずにシンプルにJSONをデータ構造化する
はじめに
モバイルアプリサービス部の中安です。
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) } }
celsius
もfahrenheit
もJSON上は文字列型なので、文字列以外でデコードするとエラーになってしまいます。
なので、イニシャライザを使って文字列で取ったものを型変換してメンバ変数に入れる流れにしています。
まとめ
この仕組みを使うと、構造体がずいぶんとすっきりになるような気がします。
Alamofireなどと相性よくコード量が減るSwiftyJSONですが、さらにコードをダイエットさせられるかもしれません。