モバイル基盤グループのヴァンサン(@vincentisambart)です。
Swift 4 で JSON を読み込むための仕組みとして Swift.Decodable
が追加されました。
iOS クックパッドアプリでは、 Swift での JSON の読込は以前 Himotokiが使われていましたが、新規コードでは Swift.Decodable
が使われています。依存関係を減らすために、 Himotoki を使っているコードが少しずつ Swift.Decodable
に移行されています。
ただし、この間、ユーザーの報告で分かったのですが、最近 Himotoki から Swift.Decodable
に移行したコード辺りに一部のユーザーにエラーが出ています。 iOS 10 に限りますが。
調査
調べてみた結果、以下のコードでエラーを再現できました。
structMyDecodable:Decodable { varid:Int64 } letstr="{\"id\":1000000000000000070}"letdata= str.data(using: .utf8)!do { letdecodable= try JSONDecoder().decode(MyDecodable.self, from:data) print("id: \(decodable.id)") } catch { print("error: \(error)") }
iOS 10 で実行してみると Parsed JSON number <1000000000000000070> does not fit in Int64.
というエラーが出ます。 1000000000000000080
でも起きますが、 1000000000000000071
では起きません。
このエラーって何だろう… Swift がオープンソースなので、コードに grep してみましょう。これっぽい。エラーが発生する条件をもう少し見てみましょう。
guardletnumber= value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at:self.codingPath, expectation:type, reality:value) } letint64= number.int64Value guard NSNumber(value:int64) == number else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath:self.codingPath, debugDescription:"Parsed JSON number <\(number)> does not fit in \(type).")) }
number
が NSNumber
なのに NSNumber(value: number.int64Value) == number
を満たさない!?
JSON の解読は実は JSONDecoder
が Foundation の JSONSerialization
を使っているので、 JSONSerialization
を直接使ってみましょう。
letstr="{\"id\":1000000000000000070}"letdata= str.data(using: .utf8)!letjsonObject= try! JSONSerialization.jsonObject(with:data) as! [NSString:Any] letnumber= jsonObject["id"] as! NSNumber print("number: \(number)") print("type: \(type(of: number))") print("comparison: \(NSNumber(value: number.int64Value) == number)")
iOS 10 で実行してみた結果は以下の通りです。
number: 1000000000000000070 type: NSDecimalNumber comparison: false
iOS 11 では以下のように表示されます。
number: 1000000000000000070 type: __NSCFNumber comparison: true
結果がかなり違いますね。 iOS 10 でもっと小さい数字を使ってみると、 iOS 11 と同じ結果になります。
number: 10000070 type: __NSCFNumber comparison: true
__NSCFNumber
というクラス名は不思議に見えるかもしれませんが、一番見かける NSNumber
のサブクラスです。 type(of: NSNumber(value: 1))
も __NSCFNumber
です。
iOS 11 で JSONSerialization
が数字に使っているクラスの条件が変わったようですね。実際 iOS 11 でも、 64-bit に入りきらない大きい数字だと NSDecimalNumber
になります。
解決方法
では、原因があの NSDecimalNumber
にあるのは分かりましたが、問題はどう解決すればいいのでしょうか。
iOS 10 の JSONSerialization
は流石に直せません。
NSDecimalNumber
と遊んでみると挙動が分かりにくいところがありますが、上記の大きい数字でも NSDecimalNumber(value: int64) == number
が満たされるので、 Swift 本体は条件を以下のにすれば直りそうです。
letint64= number.int64Value letrecreatedNumber= number is NSDecimalNumber ? NSDecimalNumber(value:int64) :NSNumber(value:int64) guard recreatedNumber == number else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath:self.codingPath, debugDescription:"Parsed JSON number <\(number)> does not fit in \(type).")) }
iOS クックパッドアプリはどうしたかと言いますと、Swift.Decodable
を使っていて、サーバーからとても大きい ID が来そうな箇所だけを Himotoki に戻すことにしました。 iOS 10 対応をやめたら、再度 Swift.Decodable
に戻す予定です。
まとめ
iOS 10 にまだ対応しているアプリは Swift.Decodable
を準拠している class
や struct
内に Int64
を使っている場合、要注意です。一部のとても大きい数字では読込中にエラーが起きる可能性があります。その場合、すぐできる対応は対象の class
や struct
で Swift.Decodable
を使うのをやめる必要あるかもしれません。
バグを報告したので、修正が行われたら追記します。