SwiftでHimotokiの使い方メモ
iOSのアプリを開発している時にSwiftでHimotokiを使った時のメモ。 執筆時点のHimotokiのバージョンは2.0.1。 自分が使うときにいい感じにまとまった情報が見つからなかったので誰かの為になればと書くことにした。
Himotokiとは
Himotoki (紐解き) is a type-safe JSON decoding library purely written in Swift. This library is highly inspired by popular JSON parsing libraries in Swift: Argo and ObjectMapper.
ということなので、タイプセーフなJSONデコーダー。デコード専門でエンコードする機能は無い。
本家のサンプルソース
gist3f4f01e767f580cfddfd5aa0629ed1dd
使い方
基本
以下のDecodable protocolを実装することになる。
public protocol Decodable { /// - Throws: DecodeError or an arbitrary ErrorType public static func decode(e: Himotoki.Extractor) throws -> Self }
本家の例ではstructだがclassでももちろん問題ない。 ルールとしてDecodableなクラスがデータ保持クラスとならなければならず、必然的に戻りもDecodableを実装したインスタンスになる。 結果としてDecodableはフィールドとデコードロジックを持つことになる。
今回の記事ではJSON形式で記載するが、実際Himotokiでデコードする場合は以下のように変換したものを渡す。
// JSON文字列 let jsonString = "{\"id\": 1, \"name\": \"taro\"}" // JSONをシリアライズ let json = try! NSJSONSerialization.JSONObjectWithData( jsonString.dataUsingEncoding(NSUTF8StringEncoding)!, options: NSJSONReadingOptions(rawValue: 0) ) // Himotokiでデコード print(try! Person.decodeValue(json))
もしくは最初からデータ形式なものでもいい。
let json:AnyJSON = [ "name": "Himotoki", "floor": 12, "owner": ["id": 1, "name": "taro"], "members": [ ["id": 1, "name": "taro", "remarks": [ "age": "18", ] ], ["id": 2, "name": "hanako"], ] ] // Himotokiでデコード print(try! Group.decodeValue(json))
演算子(Operators)
Himotokiの分かりづらい部分としては演算子がある。 初見では「これは一体何なんだ?」となるが慣れれば簡単。
Decodable protocolの引数として渡されるExtractorの演算子として以下がある。
演算子 | デコード結果 | 備考 |
---|---|---|
<| |
T |
値(単体) |
<|? |
T? |
optional(単体) |
<|| |
[T] |
値(Array) |
<||? |
[T]? |
optional(配列) |
<|-| |
[String: T] |
値(Dictionary) |
<|-|? |
[String: T]? |
optional(Dictionary) |
APIのレスポンスを例に考えると
- 必ず存在するキーの場合は「値(単体)」
e <| "name"
- 存在しない場合があるキーの場合は「optional(単体)」
e <|? "name"
- 必ず存在するキーで値が配列の場合は「値(Array)」
e <|| "emails"
- 存在しない場合があるキーで値が配列の場合は「optional(配列)」
e <||? "emails"
- 必ず存在するキーで値がハッシュの場合は「値(Dictionary)」
e <|-| "remarks"
- 存在しない場合があるキーで値がハッシュの場合は「optional(Dictionary)」
e <|-|? "remarks"
ネストしたハッシュをフラットにデコード
{ "name": { "first": "taro", "last": "tanaka" } }
上記のような階層がある場合でもフラットにデコードしたい場合は以下のように書ける。
struct Person: Decodable { let firstName: String let lastName: String static func decode(e: Extractor) throws -> Person { return try Person( firstName: e <| ["name", "first"], lastName: e <| ["name", "last"] ) } }
結果。
print(try! Person.decodeValue(json)) => Person(firstName: "taro", lastName: "tanaka")
つまり配列で、階層を表現することが出来る。
ネストしたハッシュを階層を付きでデコード
{ "name": "Himotoki", "floor": 12, "owner": {"id": 1, "name": "taro"}, "members": [ {"id": 1, "name": "taro"}, {"id": 2, "name": "hanako"} ] }
実際はフラットなハッシュではなく、親子関係を持つのも多いと思う。
子供が共通な情報だったりすることもあるのでそういう場合のデコードももちろん可能。
例えば上記のowner
(単体)やmembers
(Array)の階層部分を別のクラスとしてもデコード出来る。
その場合は親(Group)と子クラス(Person)のDecodableを作る必要がある。
struct Person: Decodable { let id: Int let name: String static func decode(e: Extractor) throws -> Person { return try Person( id: e <| "id", name: e <| "name" ) } } struct Group: Decodable { let name: String let floor: Int let owner: Person // 変数の型をPersonにする let members: [Person] // 変数の型をPersonの配列にする static func decode(e: Extractor) throws -> Group { return try Group( name: e <| "name", floor: e <| "floor", owner: e <| "owner", // 変数の型がDecodableの場合は階層的にデコードしてくれる members: e <|| "members" // もちろん配列でも問題ない ) } }
結果。
print(try! Group.decodeValue(json)) => Group( name: "Himotoki", floor: 12, owner: Person(id: 1, name: "taro"), members: [ Person(id: 1, name: "taro"), Person(id: 2, name: "hanako") ] )
Dictionary
// json1 { "id": 1, "name": "taro", "remarks": {"age": "18"} // ここをDictionaryとして受け取る } // json2 // こちらの例はremarksキーが無い(optional説明用) { "id": 1, "name": "hanako" }
structやclassを用意しないでもDictionaryとして受け取ることも出来る。 その時も一応タイプセーフとなるが、専用のstructやclassを作る程はタイプセーフ度は高くない。 動的に項目が変わる場合かつ表示もそれに合わせて対応できている時に使うのが良いかもしれない。
struct Person: Decodable { let id: Int let name: String let remarks: [String: String]? // optional系の場合は変数もoptionalにしておく必要がある static func decode(e: Extractor) throws -> Person { return try Person( id: e <| "id", name: e <| "name", remarks: e <|-|? "remarks" // 今回はoptionalにする必要は無い(「e<|-|」でいい)が例としてoptionalを使用 ) } }
結果。
// json1 print(try! Person.decodeValue(json1)) => Person(id: 1, name: "taro", remarks: Optional(["age": "18"])) // json2 print(try! Person.decodeValue(json2)) => Person(id: 2, name: "hanako", remarks: nil)
一番上の階層が配列の場合
[ {"id": 1, "name": "taro"}, {"id": 2, "name": "hanako"} ]
今まではJSONがハッシュで始まっていたが、上の例の場合はdecodeValueではなくdecodeArrayを使えば良い。 この場合はDecodableのメソッドではなくグローバルなメソッドのdecodeArray。
struct Person: Decodable { let id: Int let name: String let remarks: [String: String]? // optional系の場合は変数もoptionalにしておく必要がある static func decode(e: Extractor) throws -> Person { return try Person( id: e <| "id", name: e <| "name", remarks: e <|-|? "remarks" // 今回はoptionalにする必要は無い(「e<|-|」でいい)が例としてoptionalを使用 ) } }
結果。
// 型推論なので変数の型でPersonのArrayだと示す必要がある let people:[Person] = try! decodeArray(json) // Person.decodeArray(json)ではないことに注意 print(people) => [Person(id: 1, name: "taro", remarks: nil), Person(id: 2, name: "hanako", remarks: nil)]
蛇足だが、グローバルなdecodeValueを使っても今までのことと同じことが出来る。 その場合は型推論用に変数などに受ける必要がある。
let person:Person = try! decodeValue(json) print(person) // 以下の様に型を指定しないとAmbiguous reference to member 'decodeValue'となってコンパイルエラーとなる let person = try! decodeValue(json) print(person)
rootKeyPath
{ "name": "Himotoki", "owner": {"id": 1, "name": "taro(owner)"} "remarks": { "language": "Swift", "version: "2.0.1" }, "members": [ {"id": 1, "name": "taro"}, {"id": 2, "name": "hanako"} ] }
一番上が配列でなくても、一発で配列部分だけをデコードすることができる。 例えば上のJSONでmembersをいきなり取得したい場合は以下の様に書ける。 rootKeyPathを引数に渡すことで、JSONを辿ってデコードしてくれる。 (rootKeyPathは配列なのでさらに深くても辿ってくれる。)
let people: [Person] = try! decodeArray(json, rootKeyPath: ["members"]) print(people) => [ Person(id: 1, name: "taro", remarks: nil), Person(id: 2, name: "hanako", remarks: nil) ]
同じくハッシュ版もある。
let remarks: [String: String] = try! decodeDictionary(json, rootKeyPath: ["remarks"]) print(remarks) => ["language": "Swift", "version": "2.0.1"]
そしてもちろんDecodable版もある。
// rootKeyPathは1階層の場合は配列にしなくても良い print(try! Person.decodeValue(json, rootKeyPath: "owner")) => Person(id: 1, name: "taro(owner)", remarks: nil)
Transformation
Himotokiはデフォルトでは基本的な文字列や数値しかデコード出来ない。 URLや日時をデコードする場合は一旦Himotokiから文字列として受け取って変換してもいいが、スマートにやるのであれば独自のTransformationを書くことも出来る。
以下、本家のサンプルソース。
// Creates a `Transformer` instance. let URLTransformer = Transformer<String, NSURL> { URLString throws -> NSURL in if let URL = NSURL(string: URLString) { return URL } throw customError("Invalid URL string: \(URLString)") } let URL = try URLTransformer.apply(e <| "foo_url") let otherURLs = try URLTransformer.apply(e <|| "bar_urls")
例外について
値が返った(デコードできた)場合は型が保証される関係上、失敗した場合は即例外が発生する。
do { return try decodeValue(json) } catch let DecodeError.MissingKeyPath(keyPath) { // optionalではなく値系でキーが存在しない場合に発生する。 // keyPathにはキー情報が入っているが、ネストしたオブジェクトのパースも対応しているため配列が入っている。 // // ネストしたオブジェクトのパースとは // {"name": {"first": "taro", "last": "tanaka"}} // e <| ["name", "first"] // と書くことでtaroがデコードされる。 // その場合はkeyPath.componentsに["name", "first"]が入っている。 // 通常の場合も["name"]が入っている。 print("key: \(keyPath.components)") } catch let DecodeError.TypeMismatch(expected: expected, actual: actual, keyPath: keyPath) { // 型に変換出来なかった場合に発生する。 // expectedとactualはString型で形名が入っている。 // keyPathに関しては上の「DecodeError.MissingKeyPath」と同様。 print("description: Failed to convert JSON value to model's property.") print(" Key path \(keyPath.components) expected `\(expected)` but was `\(actual)`.") print("key : \(keyPath.components)") print("expected : \(expected)") print("actual : \(actual)") } catch let DecodeError.Custom(string) { // カスタムエラーの場合。 // Transformationなどで自分が発生させる例外。 // エラーメッセージを入れておくことが出来る。 print("error: [decode error] custom.") print(string) }
最後に
ちょっと世の中に情報が少ないので理解するのが少し大変だが、慣れれば簡単に書けるようになるので便利。