モバイル基盤グループのヴァンサン(@vincentisambart)です。
Swift 4 で JSON を読み込むための仕組みとして Swift.Decodable が追加されました。
iOS クックパッドアプリでは、 Swift での JSON の読込は以前 Himotoki が使われていましたが、新規コードでは Swift.Decodable が使われています。依存関係を減らすために、 Himotoki を使っているコードが少しずつ Swift.Decodable に移行されています。
ただし、この間、ユーザーの報告で分かったのですが、最近 Himotoki から Swift.Decodable に移行したコード辺りに一部のユーザーにエラーが出ています。 iOS 10 に限りますが。
調査
調べてみた結果、以下のコードでエラーを再現できました。
struct MyDecodable: Decodable { var id: Int64 } let str = "{\"id\":1000000000000000070}" let data = str.data(using: .utf8)! do { let decodable = 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 してみましょう。これっぽい。エラーが発生する条件をもう少し見てみましょう。
guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } let int64 = 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 を直接使ってみましょう。
let str = "{\"id\":1000000000000000070}" let data = str.data(using: .utf8)! let jsonObject = try! JSONSerialization.jsonObject(with: data) as! [NSString: Any] let number = 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 本体は条件を以下のにすれば直りそうです。
let int64 = number.int64Value let recreatedNumber = 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 を使うのをやめる必要あるかもしれません。
バグを報告したので、修正が行われたら追記します。