LoginSignup
5

More than 5 years have passed since last update.

Swift - introducing swift-json, even swiftier than SwiftyJSON

Posted at

swift-jsonをゼロから書き直したのでおしらせします。

JSON as a Swift Literal

どういうものかというと、こういうものです。

let json:JSON = [
    "null":     nil,
    "bool":     true,
    "int":      -42,
    "double":   42.195,
    "string":   "漢字、カタカナ、ひらがなと\"引用符\"の入ったstring😇",
    "array":    [nil, true, 1, "one", [1], ["one":1]],
    "object":   [
        "null":nil, "bool":false, "number":0, "string":"" ,"array":[], "object":[:]
    ],
    "url":"https://github.com/dankogai/"
]

はい。いきなりJSON型としてリテラルで書き下せます。

JSON型はCustomStringConvertibleで、その.descriptionはECMASCriptのJSON.stringify()で出力される文字列そのものです。

{
    "null":     null,
    "bool":     true,
    "int":      -42,
    "double":   42.195,
    "string":   "漢字、カタカナ、ひらがなと\"引用符\"の入ったstring😇",
    "array":    [null, true, 1, "one", [1], {"one":1}],
    "object":   {
        "null":null, "bool":false, "number":0, "string":"" ,"array":[], "object":{}
    },
    "url":"https://github.com/dankogai/"

}

初期化はリテラルからはもちろん、文字列からでもできますし…

JSON(string:"{\"swift\":[\"safe\",\"fast\",\"expressive\"]}")

URLの中身からでもできます。

JSON(urlString:"https://api.github.com")

Direct Manipulation

letではなくvarの場合、中身を直接かつ直感的に操作することも可能です。

var json = JSON([])
json[0] = nil
json[1] = true
json[2] = 1

一つ気をつけなければならないのは、右側にあるリテラルはビルトインのniltrue1ではなく、JSON型の.Null.Bool(true).Number(1)であること。よって以下はエラーになります。

let one = "one"
json[3] = one // error: cannot assign value of type 'String' to type 'JSON'

ただしこの場合は、次のようにunwrapしてしまえばいいのです。

json[1].bool   = Bool(true)
json[2].number = Int(1)
json[3].string = String("one")
json[4].array  = [Int(1)]
json[5].object = [String("one"):Int(1)]

これらは getter であるとともに setter でもあり、 getter としては、型が合えばJSON型から元の型の値を返し、合わなければnilを返すようになっています。

json[1].bool    // Optional(true)
json[1].number  // nil

その特性を活かすと、こんな書き方もできます。

json[2].number! += 1            // now 2
json[3].string!.removeLast()    // now "on"
json[4].array!.append(2)        // now [1, 2]
json[5].object!["two"] = 2      // now ["one":1,"two":2]

JSON.Arrayの場合に、存在しない添字に値を代入すると、配列が拡張され隙間は.Nullで埋まります。

json[10] = false    // json[6...9] are null

.Objectの場合に存在しないキーに対して値を代入した場合も同様です。つまり、JSON型はECMAScriptのobjectのごとく振る舞います。

Protocol Conformance

JSON型は次のプロトコルに準拠しています。

  • Equatableなので直接比較ができます
JSON(["two":2]) == JSON(["t"+"w"+"o":1+1])
  • HashableなのでDictionaryのキーとしても使えます。

  • ExpressibleBy*Literalなので、リテラルを直接書き下せます。前述の通りです。

  • CustomStringConvertible で、文字列化すると正しいJSON表記になります。

  • CodableでもあるのでJSONENcoder()の代わりになったりもします。

  • Sequenceでもあります。が、Swiftの制限により直感性が少し落ちています。

for v in JSON([nil, true, 1, "one", [1], ["one":1]]) {
   // v に入るのは?
}

上記の例だとvJSONではなく(IteratorKey,JSON)で、IteratorKeyは配列の場合は.Index、辞書の場合は.Keyとなります。それぞれアクセサーが用意されているので

for (i, v) in JSON([nil, true, 1, "one", [1], ["one":1]]) {
  // 添字は i ではなく i.index
}
for (k, v) in JSON([
        "null":nil, "bool":false, "number":0, "string":"" ,
        "array":[], "object":[:]
    ]) {
  // 添字は k ではなく k.key
}

と出来はします。が、これは先ほどの.array.objectの方が使いやすいでしょう。

for v in JSON([nil, true, 1, "one", [1], ["one":1]]).array! {
    // ...
}
for (k, v) in JSON([
        "null":nil, "bool":false, "number":0, "string":"" ,
        "array":[], "object":[:]
    ]).object! {
    // ...
}

なお当然のことですが、nullboolnumberstringは空のSequenceになります。

Error handling

init?init() throwsはJSONとは相性が悪いのでJSON型では採用せず、その代わりエラーが起こったら.Error(.ErrorType)型の値を返し、それが連鎖するようになっています。エラーになったのか、そしてなった場合はどんなエラーだったのかを確認するには次のようにすればOKです。

if let e = json.error {
    debugPrint(e.type)
    if let nsError = e.nsError {
        // do anything with nsError
    }
}

Usage

build

$ git clone https://github.com/dankogai/swift-json.git
$ cd swift-json # the following assumes your $PWD is here
$ swift build

REPL

次のようにしてREPLで実行できます。

$ scripts/run-repl.sh

REPLが上がったらimport JSONしてお楽しみ下しあ。

  1> import JSON
  2> let json:JSON = ["swift":["safe","fast","expressive"]]
json: JSON.JSON = Object {
  Object = 1 key/value pair {
    [0] = {
      key = "swift"
      value = Array {
        Array = 3 values {
          [0] = String {
            String = "safe"
          }
          [1] = String {
            String = "fast"
          }
          [2] = String {
            String = "expressive"
          }
        }
      }
    }
  }
}

Xcode

.xcodeprojはレポジトリには含まれていません。Swift Package Managerでswift package generate-xcodeprojすれば生成されるので。一応

$ scripts/prep-xcode

でそれをやった上でworkspaceを開いてくれます。

iOS and Swift Playground

現状 Swift Package Manager と macOS/Linux 以外のプラットフォームとの相性は悪く、特に Playgrounds はモジュールすらサポートしてません。しかし依存コードをSourcesフォルダーに置けば援用してくれます。モジュール本体は[JSON.swift]だけなので、それを手でコピーしても良いですし、

$ scripts/ios-prep.sh

を実行するとレポジトリのiOS/JSON.playgroundをよきに計らってくれるのであとはそれを iCloud Drive ぶっこんで同期すれば、Playgrounds for iPad でもお楽しみになれます。

IMG_0074.png

From Your SwiftPM-Managed Projects

とはいえ基本は Swift Package Manager の利用が前提です。

dependenciesに以下を、

.package(
  url: "https://github.com/dankogai/swift-json.git", from: "4.0.0"
)

.target に以下を追加したら、あとは

.target(
  name: "YourSwiftyPackage",
  dependencies: ["JSON"])
import JSON

してお楽しみください。

犯行動機

むしゃくしゃして書き直した。後悔はしていない…というのは冗談にしても、当初 swift-json は class でした。

これはJSON Schemaとしての利用を念頭においてそうしたのですが、Swift 4 でCodableが入ったことにより陳腐化しました。

それではJSONDecoderJSONEncoderさえあればいいかというとそうは行きません。フリーフォームのJSONにアクセスする需要は常にあるからです。そういうことならSwiftyJSONを使えばいいじゃないかという疑問は当然ですが、以下の理由によりSwiftyJSONでは私の需要を満たせなかったのです。

  • SwiftyJSON は framework をビルドして使うことを前提としていること。それでは Playgrounds for iOS で動かせないではないか。これが一番の理由
  • SwiftyJSON はJSONSerializationの吐いたJSONObjectつまりAnyをそのまま援用していること。要素にアクセスする都度Anyを unwrap するというのは Swifty とは言えません。対して本モジュールのJSONenumで、Anyは一切入ってません。JSONSerialization叩くのはStringからJSONへ変換するときだけで、JSONからStringへの変換はJSONSerializationに全く依存しません。別のParserがあればすぐに差し替えられます。少なくともこの一点に関しては異論なく swiftier なはず。

  • SwiftyJSONのJSON.swiftは1500行以上あるのに対し、本モジュールのそれは現時点で350行切ってます。これくらいなら前述の Playgrounds for iPad の事例のようにファイルを一つコピーすれば事足ります。

私からは以上です。 Enjoy!

Dan the Safe, Fast, and Expressive

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
5