[iOS][Swift] ArgoでJSONをパースする
Swift製のJSONをパースするライブラリは色々ありますが、今回はArgoというライブラリを取り上げたいと思います。
HaskellのAesonというライブラリからインスピレーションを受けた様で、ちょっぴり特徴のある記述(アプリカティブスタイル)が必要でありますが、慣れればシンプルな記述が出来るのかなと思います。(実際に特徴のある記述が必要なのは一緒に入れるCurryというライブラリの方です)
また、Argoはthoughtbot,inc.という会社が作っていて、
Swift Version | Argo Version |
---|---|
3.X | master |
2.2, 2.3 | 3.X |
2.0, 2.1 | 2.X |
1.2 - 2.0 | 1.X |
1.1 | 0.3.X |
のようにSwiftの各バージョンにきちんと対応していたり、きちんと更新している感があります。
(特に、今後Swift3が登場するのでライブラリが更新されていないと大変だと思われます)
ライセンスはMITです。
検証環境
今回は下記環境で試しています。
Xcode | 7.3.1 |
---|---|
Swift | 2.2 |
CocoaPods | 1.0.0 |
準備
CocoaPodsで追加します。
パースの時に使うCurryも一緒に入れます。
use_frameworks! target "ターゲット名" do pod 'Argo' pod 'Curry' end
検証用のJSONファイルを用意する
本来はAPIなどからJSONファイルを読み込みますが、今回は検証用にローカルにファイルを用意しました。
そして読み込めるようにしておきました。
func loadJson(name: String) -> AnyObject? { if let path = NSBundle.mainBundle().pathForResource(name, ofType: "json") { if let data = NSData(contentsOfFile: path) { return try? NSJSONSerialization.JSONObjectWithData(data, options: []) } } return nil }
今回のサンプルで使うJSONファイルは下記になります。(user.json)
{ "id" : 123, "name" : "夏目漱石", "email": null }
※ 値は適当です。
JSONのパースについて
JSONに変換する
ArgoにはAnyObjectインスタンスをJSONに変換するクラスが用意されています。
これは、NSJSONSerializationなどから返された弱く型付けされたオブジェクトを強く型付けされたJSONツリー構造にします。
if let result = loadJson("user") { let json = JSON(result) // 処理 }
※ loadJson("user")
は検証用のJSONファイルを用意するで記載したものです。
ArgoではこのJSONを使ってパースします。
Argo単体でパースする
公式のREADMEにはCurryを使っているサンプルが載っていますが、まずは単体で扱ってみたいと思います。
let id: Decoded<Int> = json <| "id"
let id: Decoded<Int> = json <|? "id"
jsonからデコードするには json <| "キー名"
または json <|? "キー名"
を使用します。
返ってくる値はDecoded<T>
になります。
Decoded
Decoded
成功時はSuccess(T)、失敗時はFailure(DecodeError)になります。
値を取得するには`valueを使います。
成功or失敗情報が不要な場合、下記のようにも記載が出来ます。 (デコードに成功したらidに値が入る)
let id: Int? = (json <| "id").value
<|
<|は値が取得出来ない場合(指定したキーが存在しない、指定したキーの値がNULLの時)、デコードが失敗(Failure)になります。
/* { "id" : 123, "name" : "夏目漱石", "email": null } */ let id: Decoded<Int> = json <| "id" // Success(123) let name: String? = (json <| "name").value // Optional("夏目漱石") // 変数の型とJSONの型が異なる場合 let nid: Decoded<Int> = json <| "name" // Failure(TypeMismatch(Expected Int, got String(夏目漱石))) // 値がnullの場合 let email: Decoded<String> = json <| "email" // Failure(MissingKey(email)) // キーが無い場合 let memo: Decoded<String> = json <| "memo" // Failure(MissingKey(memo))
<|?
<|?は値が取得出来ない場合(指定したキーが存在しない、指定したキーの値がNULLの時)でも、デコードが成功(Success)になります。
/* { "id" : 123, "name" : "夏目漱石", "email": null } */ let id: Decoded<Int?> = json <|? "id" // Success(123) // 変数の型とJSONの型が異なる場合 let nid: Decoded<Int?> = json <|? "name" // Failure(TypeMismatch(Expected Int, got String(夏目漱石))) // 値がnullの場合 let email: Decoded<String?> = json <|? "email" // Success(nil) // キーが無い場合 let memo: Decoded<String?> = json <|? "memo" // Success(nil)
配列を扱う
JSON内に配列が含まれていた場合のサンプルです。
{ "id" : 123, "name" : "夏目漱石", "email": null, "opus" : [ "吾輩は猫である", "坊っちゃん"] }
上記はopus項目を追加しました。
これをパースするには、 json <|| "キー名"
または json <||? "キー名"
を使用します。
(<||、<||? は、配列をデコードします。)
上記JSONの場合、
let opus: Decoded<[String]> = json <|| "opus"
または
let opus: Decoded<[String]?> = json <||? "opus"
と書くことで配列を取得することが出来ます。
また、配列の中身が下記のように構造化?していた時はどうでしょうか。
{ "id" : 123, "name" : "夏目漱石", "email": null, "opus" : [{"title": "吾輩は猫である"}, {"title": "坊っちゃん"}] }
一度、JSONの配列として取得して扱うことが出来ます。
let opus: Decoded<[JSON]> = json <|| "opus" opus.value?.forEach { if let title: String = ($0 <| "title").value { print(title) } }
バリデーションをする
JSONからデコードした値をチェックするにはflatMapを使います。
例えば、idが1以上でなければならない場合にチェックするサンプルは下記です。
// idの値を取得出来ても0以下の場合はcustomError(validation error)になる let id: Decoded<Int> = (json <| "id").flatMap { $0 > 0 ? pure($0) : .customError("validation error (\($0))") }
pure() は 成功(.Success(T))として値を返します。
エラーの場合は独自エラーとして.customError("エラー文言")を返すことが出来ます。(typeMismatchやmissingKeyで返しても別に良いですが。。。)
値が取れない場合はデフォルト値を入れる
値が取れなくてもデフォルト値を入れるには(json <|? キー名).map { $0 ?? デフォルト値 }
を使用すると良さそうです。
/* { "id" : 123, "name" : "夏目漱石", "email": null } */ // 値が取れない場合は"no data"が返る let memo: Decoded<String> = (json <|? "memo").map { $0 ?? "no data" } // Success("no data") // 値がnullの場合は""を返す let email: Decoded<String> = (json <|? "email").map { $0 ?? "" } // Success("")
Curryを使って簡易化する
一般的にJSONから返ってきた値は構造体(struct)に格納することが多いと思います。
今回は下記のように定義をしました。
struct User { let id: Int let name: String let email: String let memo: String? }
Curryを使わない例
まずはCurryを使わない場合の例です。
import Argo struct User { let id: Int let name: String let email: String let memo: String? } extension User { static func decode(json: JSON) -> User? { let id: Decoded<Int> = (json <| "id").flatMap { $0 > 0 ? pure($0) : .customError("validation error (\($0))") } let name: Decoded<String> = json <| "name" let email: Decoded<String> = (json <|? "email").map { $0 ?? "" } let memo: String? = (json <| "memo").value guard let valueID = id.value, valueName = name.value, valueEmail = email.value else { return nil } let user = User.init(id: valueID, name: valueName, email: valueEmail, memo: memo) return user } }
もっとマシに書けるかもしれませんが、上記の様に一旦値を取得して、チェックして、Userに格納してといった手順が必要になります。
また、上記の例だと呼び出し元には失敗時のエラー情報が返らないので、何で失敗したかを判断することが出来ません。
Curryを使った例
アプリカティブスタイルを使うことでシンプルに書くことが出来ます。
import Argo import Curry struct User { let id: Int let name: String let email: String let memo: String? } extension User: Decodable { static func decode(json: JSON) -> Decoded<User> { return curry(User.init) <^> (json <| "id").flatMap { $0 > 0 ? pure($0) : .customError("validation error (\($0))") } <*> json <| "name" <*> (json <|? "email").map { $0 ?? "" } <*> json <|? "memo" } }
Curryを使うと、
User.init(id: `id`, name: `name`, email: `email`, memo: `memo`)
の部分を
curry(User.init) <^> `id` <*> `name`<*> `email`<*> `memo`
の様に記述します。(※そのまま置き換えている訳ではないです)
一番の特徴としては、curryの方だとArgoでデコードしたものを直接入れることが出来ます。
※ アプリカティブスタイルについての詳しい説明はこちらのサイトが判りやすいです。
また、成功or失敗も判別出来るのでパースする際はCurry導入をオススメします。
注意
initの引数が多い場合、Expression was too complex to be solved in reasonable time
エラーが出る可能性があります。
その場合はモデルをネストするなどして引数の数を減らすか、Curryを使うのを諦める必要があります。