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
一つ気をつけなければならないのは、右側にあるリテラルはビルトインのnil
やtrue
や1
ではなく、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 に入るのは?
}
上記の例だとv
はJSON
ではなく(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! {
// ...
}
なお当然のことですが、null
やbool
やnumber
やstring
は空の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 でもお楽しみになれます。
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が入ったことにより陳腐化しました。
それではJSONDecoder
とJSONEncoder
さえあればいいかというとそうは行きません。フリーフォームのJSONにアクセスする需要は常にあるからです。そういうことならSwiftyJSONを使えばいいじゃないかという疑問は当然ですが、以下の理由によりSwiftyJSONでは私の需要を満たせなかったのです。
- SwiftyJSON は framework をビルドして使うことを前提としていること。それでは Playgrounds for iOS で動かせないではないか。これが一番の理由
- SwiftyJSON はJSONSerializationの吐いた
JSONObject
つまりAny
をそのまま援用していること。要素にアクセスする都度Any
を unwrap するというのは Swifty とは言えません。対して本モジュールのJSON
はenum
で、Any
は一切入ってません。JSONSerialization叩くのはString
からJSON
へ変換するときだけで、JSON
からString
への変換はJSONSerialization
に全く依存しません。別のParserがあればすぐに差し替えられます。少なくともこの一点に関しては異論なく swiftier なはず。
- SwiftyJSONのJSON.swiftは1500行以上あるのに対し、本モジュールのそれは現時点で350行切ってます。これくらいなら前述の Playgrounds for iPad の事例のようにファイルを一つコピーすれば事足ります。
私からは以上です。 Enjoy!
Dan the Safe, Fast, and Expressive