JAYSON
SwiftyJSONがなかなかSwift3を対応しない中、(もう対応されましたが)
もともとSwiftyJSONにはすこし気に入らないところが幾つかあったため、
とうとうJSONライブラリを作ってしまいました。
(SwiftはJSONパーサーが大量に存在しているので車輪の再発明
っぽくなりそうで避けてました)
名前は JAYSON です
https://github.com/muukii/JAYSON
(GitHubスターを頂けるとうれしいです... ⭐️⭐️⭐️⭐️⭐️)
JAYSONは
SwiftyJSONのような使い勝手であるEasy-Readと
try-catchを利用した厳格に値を取得するStrict-Readを用意しています。
簡単なJSONを用意して説明していきます。
{
"shots" :
[
{
"id": "itdkHUjTuIY",
"created_at": "2016-09-24T04:06:19-04:00",
"width": 4256,
"height": 2832,
"color": "#F7DCE2",
"likes": 3,
"liked_by_user": false,
"user": {
"id": "xzxvjf",
"username": "muukii",
"name": "hiroshi kimura",
"profile_image": {
"small": "https://...",
"medium": "https://...",
"large": "https://..."
},
},
},
{
"id": "irqwHfqweuIY",
"created_at": "2016-09-24T04:06:19-04:00",
"width": 4256,
"height": 2832,
"color": "#DDDCE2",
"likes": 5,
"liked_by_user": false,
"user": {
"id": "zxcvfsad3",
"username": "john",
"name": "john estropia",
"profile_image": {
"small": "https://...",
"medium": "https://...",
"large": "https://..."
},
},
}
]
}
読み込む準備
JAYSONオブジェクトを作る方法はいくつか用意しています。
- JSONのDataから生成 (APIレスポンス等をそのまま入れるイメージ)
- 無効なDataが入ってきた場合 throw します。
let jsonData: Data
let jayson = try JAYSON(data: jsonData)
- 事前にJSONSerializationを行ったAnyオブジェクトから生成
- AnyがJSONとして認識できない場合 throw します。
let jsonData: Data
let json: Any = try JSONSerialization.jsonObject(with: data, options: [])
let jayson = try JAYSON(any: json)
let userInfo: [AnyHashable: Any]
let jayson = try JAYSON(any: json)
let objects: [Any]
let jayson = try JAYSON(any: json)
- 以下の型の場合は必ず生成に成功するため
try
は不要です。
let object: [String : JAYSON]
let jayson = JAYSON(object)
let object: [JAYSON]
let jayson = JAYSON(object)
let object: [JAYSONWritableType]
let jayson = JAYSON(object)
let object: [String : JAYSONWritableType]
let jayson = JAYSON(object)
Easy Read
shots -> 0 -> user -> profile_image -> large を取得
SwiftyJSONと同様にsubscriptでアクセスできます。
key, indexが見つからなかった場合
NSNullが入ったJAYSONが返却されます。 ( JAYSON.null
と同じ状態)
SwiftyJSONとの違いは型が間違っている場合にdefaultValueを返却するプロパティを用意していないことです。
必ずOptional値の返却になります。
let urlString: String? = jayson["shots"][0]["user"]["profile_image"]["large"].string
(おまけ) shots以下を配列として取得
let shots: [JAYSON]? = jayson["shots"].array
let shots = jayson["shots"].array?.map { $0["id"].string }
// => Optional([Optional("itdkHUjTuIY"), Optional("irqwHfqweuIY")])
Strict Read
shots -> 0 -> user -> profile_image -> large を取得
こちらはJAYSONの特徴です。
key, indexが見つからなかったタイミング or 型が間違っていたタイミング
で JAYSONError
がthrowされます。
let urlString: String = try jayson
.next("shots")
.next(0)
.next("user")
.next("profile_image")
.next("large")
.getString()
(おまけ)
let shots = try jayson.next("shots").getArray().map { try $0.next("id").getString() }
// ["itdkHUjTuIY", "irqwHfqweuIY"]
Get current path
主にデバッグ時に使用するであろう機能です。
JAYSONはSwiftyJSONと同様にJAYSONオブジェクトを返却し続けます。
これはEasy, Strict どちらでも同じ挙動です。
let shots: JAYSON = jayson["shots"]
let firstShot: JAYSON = shots[0]
let id: JAYSON = firstShot["id"]
SwiftyJSONでの問題は深い階層のJSONをパースする際に発生したエラーは何が原因だったのが見つけづらいところでした。
そこでJAYSONではkey, indexによって生成されたJAYSONは親のJAYSONを保持するようにしています。
この仕組を利用して、あるJAYSONはどのようなpathを辿って作られたのかを知ることが出来ます。
let large: JAYSON = jayson["shots"][0]["user"]["profile_image"]["large"]
print(large.currentPath())
// Root->["shots"][0]["user"]["profile_image"]["large"]
Strict-Read時ではthrowされたエラーから失敗したJAYSONが届くので
どこでなにが失敗したのかを処理の上流で知ることが出来ます。
Custom Getter
URLやDateはJSONの仕様には定められていないため、文字列と同じ扱いです。
アプリではURL, Dateが存在するため、取得時に変換してしまいたいものです。
ただ、JAYSONの仕様はシンプルにしておきたいため、このようなproperty, getterは用意していません。
(プロジェクトごとにどのような変換を行い取得するかを定義するのがベターです。)
2つの方法を用意しています。
- その場にclosureで定義する
let url: URL = try jayson
.next("shots")
.next(0)
.next("user")
.next("profile_image")
.next("large")
.get { (jayson) throws -> URL in
URL(string: try jayson.getString())!
}
- Decoderを定義して
get<T>(with: Decoder<T>)
に渡す
let urlDecoder = Decoder<URL> { (jayson) throws -> URL in
URL(string: try jayson.getString())!
}
let url: URL = try jayson
.next("shots")
.next(0)
.next("user")
.next("profile_image")
.next("large")
.get(with: urlDecoder)
Build JSON
JSONパーサーはかなりの数が公開されていますが、
JSONを読み込む専用で、JSONを書き出す機能がないものが多いかもしれません。
僕は開発しているプロジェクトでAPIリクエストのBodyがJSONだったので、
1つのライブラリで読み書きが出来た方が良かったので書き込み機能を用意しました。
こちらも基本的にSwiftyJSONと同様ではありますが、
なんでもJAYSONに入るわけではなくJASONWritableType
プロトコルを持ったオブジェクトのみになります。
(出来る限りランタイムの失敗を少なくするためです)
(書き込みにもStrictモードを用意して良いかもしれません)
var jayson = JAYSON()
jayson["id"] = 18737649
jayson["active"] = true
jayson["name"] = "muukii"
jayson["images"] = JAYSON([
"large" : "http://...foo",
"medium" : "http://...foo",
"small" : "http://...foo",
])
let data: Data = try jayson.data(options: .prettyPrinted)
- 出力
{
"name" : "muukii",
"active" : true,
"id" : 18737649,
"images" : {
"large" : "http:\/\/...foo",
"small" : "http:\/\/...foo",
"medium" : "http:\/\/...foo"
}
}
リアルなサンプル
ちょっとだけリアルなサンプルを用意してみました。
dribbble APIの結果をパースする処理です。
let urlDecoder = Decoder<URL> { (jayson) throws -> URL in
URL(string: try jayson.getString())! // 任意のエラーをthrowできます。
}
struct Shot {
let id: Int
let title: String
let width: Int
let height: Int
let hidpiImageURLString: URL?
let normalImageURLString: URL
let teaserImageURLString: URL
init(jayson: JAYSON) throws {
let imagesJayson = try jayson.next("images")
id = try jayson.next("id").getInt()
title = try jayson.next("title").getString()
width = try jayson.next("width").getInt()
height = try jayson.next("height").getInt()
hidpiImageURLString = try? imagesJayson.next("hidpi").get(with: urlDecoder)
normalImageURLString = try imagesJayson.next("normal").get(with: urlDecoder)
teaserImageURLString = try imagesJayson.next("teaser").get(with: urlDecoder)
}
}
do {
let shots: [Shot] = try jayson.getArray().map(Shot.init(jayson: ))
print(shots)
} catch {
print(error) // 何個目のJAYSONでどのようなpathで失敗したのかがわかります。
}
少しだけnext()などの記述が長く感じるかもしれませんが、
それはプロジェクトごとにoperatorを用意しても良いかもしれません。
今のところ、operatorを使うとメソッドチェーンが行いづらくなるためライブラリ側では用意していません。
補足
Decodable
のような変換対象の型に採用させるprotocolを用意せず、
Decoder
しか用意していない理由は、その型に変換する方法は一つとは限らないためです。
例えばDate型では、ISO8601やUnixTimeStampなど様々な型があります。
APIによっては混在しているケースもあるかもしれません。(あんまりないとは思いますが)
Decoderを渡すようにしておけば、このような場合にも柔軟に対応できます。
おわりに
もし気に入って頂けたらGitHubスターを付けて頂けるとうれしいです!
追記
- 0.6.0にて暫定的ではありますがLinuxのサポートをしました。