Edited at

subscriptを活用してSwiftyにXMLやJSONのパスを記述する

More than 3 years have passed since last update.


イントロ

Swift製JSONパーサーとしてよく使わせるライブラリの一つにSwiftyJSONがあります。

このライブラリでは、JSONオブジェクトに対して以下の様なパスの書き方ができます。

let jsonStr = "{ \"animal\": { \"element\": [{\"name\": \"cat\"}, {\"name\": \"dog\"}] } }"

let json = JSON.parse(jsonStr)

// パスのデータにアクセスする
let name = json["animal", "element", 1, "name"].string // -> "dog"

目的データまでのパスを、配列からデータを取り出すような形で指定しています。これは、Swiftが持つSubscriptsという仕組みを上手く活用することで実現されます。どのようにすればできるのかライブラリのソースコードを基にまとめてみました。


自分で作ったクラスにSubscriptsを行う

Swiftにはsubscriptという予約語があり、角括弧を自作クラスの中で使って配列のようなアクセスが実現できます。

引数には任意の型を指定できます。例えばDictionaryのように文字列をキーとしたオブジェクトの返却などもできます。


class SomeClass {
subscript(key key: String) -> SomeClass {
...
}

subscript(index index) -> SomeClass {
...
}
}

SwiftyJSONではJSONというクラスの中に文字列や数値に対するsubscriptを実装しています。

そのため、角括弧でJSONのキーやIndex番号にアクセスが可能です。

let jsonStr = "{ \"animal\": { \"element\": [{\"name\": \"cat\"}, {\"name\": \"dog\"}] } }"

let json = JSON.parse(jsonStr)

let subJSON = json["animal"]["element"][1]

具体的な実装箇所はSwiftJSON.swiftのL470-L489の箇所です。StringとIntを引数として受け取るsubscriptが実装されています。

https://github.com/SwiftyJSON/SwiftyJSON/blob/master/Source/SwiftyJSON.swift#L446L489


SwiftyJSON.swift

extension JSON {

// ...
// (省略)
// ...
private subscript(index index: Int) -> JSON {
// ...
}

// 引数が文字列でJSONオブジェクトを返す。
private subscript(key key: String) -> JSON {
// ...
}
// ...
// (省略)
// ...
}


ただしprivateメソッドとして作られていて、外からアクセスできるようにはなっていません。実際にはSwiftyJSON.swiftのL520から始まる以下の2つのsubscriptを使ってアクセスします。次の節からこの2つについて説明します。


SwiftyJSON.swift

public subscript(path: [JSONSubscriptType]) -> JSON

public subscript(path: JSONSubscriptType...) -> JSON

https://github.com/SwiftyJSON/SwiftyJSON/blob/master/Source/SwiftyJSON.swift#L520L556


配列や可変長引数を受け取れるようにする

Subscriptsが任意の型を受け取れるということは、配列を渡すこともできます。

class SomeClass {

subscript(path: [String]) -> SomeClass {
...
}
}

また、Swiftは引数の末尾へ...をつけて可変長引数を渡せるようにできます。

class SomeClass {

subscript(path: String...) -> SomeClass {
...
}
}

そうすると、以下のように角括弧を使って複数の文字列を渡すことができます。

let obj = SomeClass()

// Array
let path = ["hoge", "fuga"]
let data1 = obj[path]

// Variadic
let data2 = obj["hoge", "fuga"]

SwiftyJSONはL520-L557でそのsubscriptを記述しています。

https://github.com/SwiftyJSON/SwiftyJSON/blob/master/Source/SwiftyJSON.swift#L520L557


SwiftyJSON.swift

extension JSON {

// ...
// (省略)
// ...
public subscript(path: [JSONSubscriptType]) -> JSON {
// ...
}

public subscript(path: JSONSubscriptType...) -> JSON {
// ...
}
// ...
// (省略)
// ...
}


では、JSONSubscriptTypeというのはなんでしょうか。


Protocolを使って複数の型を取れるようにする

配列や可変長引数を使うことで、subscriptの引数に複数の値を組み合わせて渡せるようになりました。ただし、その場合に渡せる型は1種類に限定されます。JSONの場合、配列とハッシュを組み合わせてデータ構造を構成するため、ハッシュのキーと配列のIndex番号を一緒にして渡すことができれば便利です。そのために、SwiftyJSONではJSONSubscriptTypeというProtocolを作り、それに従っている型はsubscriptへ渡せるようにしています。L427-L441を読むと、Int・Stringがそれによって拡張されています。

https://github.com/SwiftyJSON/SwiftyJSON/blob/master/Source/SwiftyJSON.swift#L427L441


SwiftyJSON.swift


// subscriptに指定できるProtocolを定義
public protocol JSONSubscriptType {
var jsonKey:JSONKey { get }
}

extension Int: JSONSubscriptType {
...
}

extension String: JSONSubscriptType {
...
}


こうすることで、IntとStringをカンマで繋いで、パス指定をすることが可能になります。

let jsonStr = "{ \"animal\": { \"element\": [{\"name\": \"cat\"}, {\"name\": \"dog\"}] } }"

let json = JSON.parse(jsonStr)

// Array
let path: [JSONSubscriptType] = ["animal", "element", 1, "name"]
let name = json[path].string // -> "dog"

// Variadic
let name = json["animal", "element", 1, "name"].string // -> "dog"


まとめ

以上のようにSwiftyJSONはsubscriptの性質をうまく利用することにより、ArrayやDictinaryを扱うようにJSONデータにアクセスできるようにしています。

ポイントをまとめると次のようになります。


  • subscriptキーワードでJSONデータに"[]"でのアクセスを可能にする

  • subscriptへ可変長引数を渡せるようにして、パス指定を実現

  • 引数の型をProtocolにすることで、StringとIntの両方をまとめて渡せるようにする

subscriptの使い方に関しては、こちらの記事がさらに網羅的にまとめているので合わせて参照してください。

また、以前仕事でこれらの仕組みを使ったXMLパーサーを開発しました。OSSとしても公開されているので、もしよろしければご利用ください。