LoginSignup
29
26

More than 5 years have passed since last update.

[Linux対応]SwiftyJSONより厳しめなJSONライブラリ作りました (JAYSON)

Last updated at Posted at 2016-09-24

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のサポートをしました。
29
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
26