Quantcast
Channel: クックパッド開発者ブログ
Viewing all articles
Browse latest Browse all 726

Swift.Decodable + Int64 / iOS 10 = 要注意

$
0
0

モバイル基盤グループのヴァンサン(@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)."))
}

numberNSNumberなのに 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を準拠している classstruct内に Int64を使っている場合、要注意です。一部のとても大きい数字では読込中にエラーが起きる可能性があります。その場合、すぐできる対応は対象の classstructSwift.Decodableを使うのをやめる必要あるかもしれません。

バグを報告したので、修正が行われたら追記します。


Viewing all articles
Browse latest Browse all 726

Trending Articles