先日のWWDCのWhat"s New in FoundationでCodableについて触れられていたので早速使ってみて、これは良いと思ったので今までお世話になっていたHimotokiから引っ越しを決意したものの、、ちょっと壁にぶつかりました..
※普通に使う分には全く困らないと思います
ネストしているケースで壁にぶつかった
例えば、以下のようなJSON(Data),JSONDecoderがあったとします
var json: String = """
{
"result": {
"persons": [
{
"name": "taro",
"age": 25
},
{
"name": "hanako",
"age": 23
}
],
"code": 0,
"flag": true
}
}
"""
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
これをSwift4のDecodableを使って素直にパースしようと思うと、以下のような感じになります。
struct PersonsResponse: Decodable {
struct Result: Decodable {
struct Person: Decodable {
let name: String
let age: Int
}
let persons: [Person]
let code: Int
let flag: Bool
}
let result: Result
}
do {
let response = try decoder.decode(PersonsResponse.self, from: data)
// ...
} catch let error {
print(error)
}
いたって普通ですね。普通なのですが…
result.persons
内だけパースして使いたいなって場合に困ってきます。
つまり、
struct Person: Decodable {
let name: String
let age: Int
}
// ※正しくないコードです
do {
let persons = try decoder.decode([Person].self, from: data)
// ...
} catch let error {
print(error)
}
としたいのですが、このままだとうまくいきません。
keyPathを渡してdecodeできるようにする
JSONDecoderにextensionを追加してみます。
extension JSONDecoder {
func decode<T: Decodable>(_ type: T.Type, from data: Data, keyPath: String) throws -> T {
let topLevel = try JSONSerialization.jsonObject(with: data)
if let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) {
let nestedJsonData = try JSONSerialization.data(withJSONObject: nestedJson)
return try decode(type, from: nestedJsonData)
} else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Nested json not found for key path \"\(keyPath)\""))
}
}
}
一度JSONSerializationでdataからjsonオブジェクトを作り、keyPathを使ってネストされたjsonを取得し、再度dataを作ってから本来のdecode(_:from:)
関数を呼び出してあげます。
エラーは一旦DecodingErrorに統一しようとdataCorrupted
を使っていますが、別のエラーとして返してあげてもよいのかもしれません🤔
これで準備が整ったので、keyPathとしてresult.persons
を渡してdecodeします。
struct Person: Decodable {
let name: String
let age: Int
}
// just works!
do {
let persons = try decoder.decode([Person].self, from: data, keyPath: "result.persons")
// ...
} catch let error {
print(error)
}
パフォーマンスは?
一度デシリアライズして、ネストしたjsonをkeyPathで取得した後に再度シリアライズしているため、素直にパースした場合と、今回のケースでは差がでてきます。
かなり雑多ですが、単純にそれぞれのケースを1000回ループさせて実行させて計測しました。Date
使っているのはご了承を。
let date = Date()
for _ in 0..<1000 {
do {
let _ = try decoder.decode(PersonsResponse.self, from: data)
} catch let error {
print(error)
}
}
print("1:", Date().timeIntervalSince(date))
// ===============================================
let date = Date()
for _ in 0..<1000 {
do {
let _ = try decoder.decode([Person].self, from: data, keyPath: "result.persons")
} catch let error {
print(error)
}
}
print("2:", Date().timeIntervalSince(date))
ケース | 1000回実行した場合の時間(秒) |
---|---|
素直にパースした場合 | 0.0513710379600525 |
keyPathで必要な部分だけパースした場合 | 1.99045598506927 |
お、おそい、、
結構な差が出てしまいますね.
decodeって何してるの?
ここを見る限り、最初にJSONSerializationでデシリアライズしているようです。
// https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/JSONEncoder.swift#L884L888 から引用
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel = try JSONSerialization.jsonObject(with: data)
let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
return try T(from: decoder)
}
なので、今回keyPath渡してパースするケースの場合、デシリアライズして、シリアライズして渡して、それがデシリアライズされて…とかなり非効率になっていますね。
_JSONDecoder
を直接触れたら…
まとめ
もちろん、result.persons
ってならないようにJSONを構築するのも大事ですが、例えば
https://developer.github.com/v3/search/#search-repositories
を叩いた結果のうち、items
以下だけパースして使いたい、なんて場合だとやっぱり困ってしまうんですよね。
JSONの構造通り構造体用意しろ! っていうのが正しい気はするものの、、悩ましい。
ちなみにHimotokiだと
let persons: [Person] = try decodeArray(json, rootKeyPath: ["result", "persons"])
とできます。
Decodable使うか、Himotoki使うか悩みますね。
そしてより良い方法を知ってる人がいたら是非教えてください。
Gistはこちらです。