Skip to content

Commit 953b800

Browse files
committed
JSONSerialization: Improve number parsing for JSON
- Add _JSONNumber and _NSJSONNumber, an internal subclass of NSNumber, to provide lazy parsing of numbers at time of use. This avoids parsing numbers as Decimal when later bridged to Double/Float and vice-versa which has accuracy issues if converted from one to the other. - Add test cases for SR-7054 and SR-12244
1 parent 94691e3 commit 953b800

13 files changed

+1159
-182
lines changed

Foundation.xcodeproj/project.pbxproj

+17-1
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,15 @@
398398
B910957B1EEF237800A71930 /* NSString-UTF16-BE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B91095791EEF237800A71930 /* NSString-UTF16-BE-data.txt */; };
399399
B91161AA2429860900BD2907 /* DataURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91161A82429857D00BD2907 /* DataURLProtocol.swift */; };
400400
B91161AD242A363900BD2907 /* TestDataURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91161AB242A350D00BD2907 /* TestDataURLProtocol.swift */; };
401+
B9292465258E75DD00E24DA5 /* JSONNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9292464258E75DD00E24DA5 /* JSONNumber.swift */; };
402+
B929246F258E772B00E24DA5 /* CMakeLists.txt in Resources */ = {isa = PBXBuildFile; fileRef = B929246E258E772B00E24DA5 /* CMakeLists.txt */; };
401403
B933A79E1F3055F700FE6846 /* NSString-UTF32-BE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B933A79C1F3055F600FE6846 /* NSString-UTF32-BE-data.txt */; };
402404
B933A79F1F3055F700FE6846 /* NSString-UTF32-LE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B933A79D1F3055F600FE6846 /* NSString-UTF32-LE-data.txt */; };
403405
B940492D223B146800FB4384 /* TestProgressFraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940492C223B146800FB4384 /* TestProgressFraction.swift */; };
404406
B94B063C23FDE2BD00B244E8 /* SwiftFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5D885D1BBC938800234F36 /* SwiftFoundation.framework */; };
405407
B951B5EC1F4E2A2000D8B332 /* TestNSLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B951B5EB1F4E2A2000D8B332 /* TestNSLock.swift */; };
408+
B959016E25970BE300CACAE3 /* TestJSONNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = B959016D25970BE300CACAE3 /* TestJSONNumber.swift */; };
409+
B95901922597102000CACAE3 /* CMakeLists.txt in Resources */ = {isa = PBXBuildFile; fileRef = B95901912597102000CACAE3 /* CMakeLists.txt */; };
406410
B95FC97622B84B0A005DEA0A /* TestNSSortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 152EF3932283457B001E1269 /* TestNSSortDescriptor.swift */; };
407411
B96C10F625BA1EFD00985A32 /* NSURLComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96C10F525BA1EFD00985A32 /* NSURLComponents.swift */; };
408412
B96C110025BA20A600985A32 /* NSURLQueryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96C10FF25BA20A600985A32 /* NSURLQueryItem.swift */; };
@@ -1109,10 +1113,14 @@
11091113
B91095791EEF237800A71930 /* NSString-UTF16-BE-data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "NSString-UTF16-BE-data.txt"; sourceTree = "<group>"; };
11101114
B91161A82429857D00BD2907 /* DataURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataURLProtocol.swift; sourceTree = "<group>"; };
11111115
B91161AB242A350D00BD2907 /* TestDataURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataURLProtocol.swift; sourceTree = "<group>"; };
1116+
B9292464258E75DD00E24DA5 /* JSONNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONNumber.swift; sourceTree = "<group>"; };
1117+
B929246E258E772B00E24DA5 /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = "<group>"; };
11121118
B933A79C1F3055F600FE6846 /* NSString-UTF32-BE-data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NSString-UTF32-BE-data.txt"; sourceTree = "<group>"; };
11131119
B933A79D1F3055F600FE6846 /* NSString-UTF32-LE-data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NSString-UTF32-LE-data.txt"; sourceTree = "<group>"; };
11141120
B940492C223B146800FB4384 /* TestProgressFraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestProgressFraction.swift; sourceTree = "<group>"; };
11151121
B951B5EB1F4E2A2000D8B332 /* TestNSLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSLock.swift; sourceTree = "<group>"; };
1122+
B959016D25970BE300CACAE3 /* TestJSONNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestJSONNumber.swift; sourceTree = "<group>"; };
1123+
B95901912597102000CACAE3 /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = "<group>"; };
11161124
B95FC97222AF0050005DEA0A /* SwiftXCTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftXCTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
11171125
B95FC97422AF051B005DEA0A /* xcode-build.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "xcode-build.sh"; sourceTree = "<group>"; };
11181126
B96C10F525BA1EFD00985A32 /* NSURLComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSURLComponents.swift; sourceTree = "<group>"; };
@@ -1777,6 +1785,7 @@
17771785
F023071023F0976B0023DBEC /* Foundation */ = {
17781786
isa = PBXGroup;
17791787
children = (
1788+
B95901912597102000CACAE3 /* CMakeLists.txt */,
17801789
155D3BBB22401D1100B0D38E /* FixtureValues.swift */,
17811790
616068F2225DE5C2004FCC54 /* FTPServer.swift */,
17821791
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */,
@@ -1819,6 +1828,7 @@
18191828
EA66F63E1BF1619600136161 /* TestIndexSet.swift */,
18201829
63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */,
18211830
3EA9D66F1EF0532D00B362D6 /* TestJSONEncoder.swift */,
1831+
B959016D25970BE300CACAE3 /* TestJSONNumber.swift */,
18221832
5EB6A15C1C188FC40037DCB8 /* TestJSONSerialization.swift */,
18231833
BD8042151E09857800487EB8 /* TestLengthFormatter.swift */,
18241834
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */,
@@ -2042,6 +2052,7 @@
20422052
F023072323F0A6E50023DBEC /* Foundation */ = {
20432053
isa = PBXGroup;
20442054
children = (
2055+
B929246E258E772B00E24DA5 /* CMakeLists.txt */,
20452056
F023072523F0B4890023DBEC /* Headers */,
20462057
F023072423F0B4140023DBEC /* Resources */,
20472058
EADE0B4D1BD09E0800C49C64 /* AffineTransform.swift */,
@@ -2081,6 +2092,7 @@
20812092
5B8BA1611D0B773A00938C27 /* IndexSet.swift */,
20822093
63DCE9D11EAA430100E9CB02 /* ISO8601DateFormatter.swift */,
20832094
3EDCE5091EF04D8100C2EC04 /* JSONEncoder.swift */,
2095+
B9292464258E75DD00E24DA5 /* JSONNumber.swift */,
20842096
EADE0B641BD15DFF00C49C64 /* JSONSerialization.swift */,
20852097
49D55FA025E84FE5007BD3B3 /* JSONSerialization+Parser.swift */,
20862098
EADE0B661BD15DFF00C49C64 /* LengthFormatter.swift */,
@@ -2100,6 +2112,7 @@
21002112
5BDC3FCD1BCF17D300ED97BB /* NSCFDictionary.swift */,
21012113
5BDC3FCF1BCF17E600ED97BB /* NSCFSet.swift */,
21022114
5BDC3FCB1BCF177E00ED97BB /* NSCFString.swift */,
2115+
15CA750924F8336A007DF6C1 /* NSCFTypeShims.swift */,
21032116
5BDC3F311BCC5DCB00ED97BB /* NSCharacterSet.swift */,
21042117
5BDC3F321BCC5DCB00ED97BB /* NSCoder.swift */,
21052118
EADE0B551BD15DFF00C49C64 /* NSComparisonPredicate.swift */,
@@ -2122,7 +2135,6 @@
21222135
D3BCEB9F1C2F6DDB00295652 /* NSKeyedCoderOldStyleArray.swift */,
21232136
D39A14001C2D6E0A00295652 /* NSKeyedUnarchiver.swift */,
21242137
5BDC3F3B1BCC5DCB00ED97BB /* NSLocale.swift */,
2125-
15CA750924F8336A007DF6C1 /* NSCFTypeShims.swift */,
21262138
5BDC3F3C1BCC5DCB00ED97BB /* NSLock.swift */,
21272139
D3BCEB9D1C2EDED800295652 /* NSLog.swift */,
21282140
5BECBA391D1CAE9A00B39B1F /* NSMeasurement.swift */,
@@ -2713,6 +2725,7 @@
27132725
isa = PBXResourcesBuildPhase;
27142726
buildActionMask = 2147483647;
27152727
files = (
2728+
B929246F258E772B00E24DA5 /* CMakeLists.txt in Resources */,
27162729
F023072623F0B4890023DBEC /* Headers in Resources */,
27172730
B983E32C23F0C69600D9C402 /* Docs in Resources */,
27182731
B983E32E23F0C6E200D9C402 /* CONTRIBUTING.md in Resources */,
@@ -2743,6 +2756,7 @@
27432756
D3A597F71C3415CC00295652 /* NSKeyedUnarchiver-ArrayTest.plist in Resources */,
27442757
CE19A88C1C23AA2300B4CB6A /* NSStringTestData.txt in Resources */,
27452758
E1A03F361C4828650023AF4D /* PropertyList-1.0.dtd in Resources */,
2759+
B95901922597102000CACAE3 /* CMakeLists.txt in Resources */,
27462760
E1A3726F1C31EBFB0023AF4D /* NSXMLDocumentTestData.xml in Resources */,
27472761
E1A03F381C482C730023AF4D /* NSXMLDTDTestData.xml in Resources */,
27482762
D3A598041C349E6A00295652 /* NSKeyedUnarchiver-OrderedSetTest.plist in Resources */,
@@ -2888,6 +2902,7 @@
28882902
5BF7AEC01BCD51F9008F214A /* NSUUID.swift in Sources */,
28892903
5BF7AEB01BCD51F9008F214A /* NSLocale.swift in Sources */,
28902904
EADE0BA31BD15E0000C49C64 /* NSKeyedArchiver.swift in Sources */,
2905+
B9292465258E75DD00E24DA5 /* JSONNumber.swift in Sources */,
28912906
5BF7AEAD1BCD51F9008F214A /* NSError.swift in Sources */,
28922907
EADE0BB61BD15E0000C49C64 /* NSSortDescriptor.swift in Sources */,
28932908
5BF7AEA41BCD51F9008F214A /* Bundle.swift in Sources */,
@@ -3104,6 +3119,7 @@
31043119
90E645DF1E4C89A400D0D47C /* TestNSCache.swift in Sources */,
31053120
5B13B34A1C582D4C00651CE2 /* TestURL.swift in Sources */,
31063121
EA54A6FB1DB16D53009E0809 /* TestObjCRuntime.swift in Sources */,
3122+
B959016E25970BE300CACAE3 /* TestJSONNumber.swift in Sources */,
31073123
BB3D7558208A1E500085CFDC /* Imports.swift in Sources */,
31083124
5B13B34D1C582D4C00651CE2 /* TestNSUUID.swift in Sources */,
31093125
15F10CDC218909BF00D88114 /* TestNSCalendar.swift in Sources */,

Sources/Foundation/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ add_library(Foundation
3636
IndexSet.swift
3737
ISO8601DateFormatter.swift
3838
JSONEncoder.swift
39+
JSONNumber.swift
3940
JSONSerialization.swift
4041
JSONSerialization+Parser.swift
4142
LengthFormatter.swift

Sources/Foundation/JSONEncoder.swift

+47-54
Original file line numberDiff line numberDiff line change
@@ -1318,14 +1318,14 @@ extension JSONDecoderImpl: Decoder {
13181318
}
13191319

13201320
private func unwrapDecimal() throws -> Decimal {
1321-
guard case .number(let numberString) = self.json else {
1321+
guard case .number(let jsonNumber) = self.json else {
13221322
throw DecodingError.typeMismatch(Decimal.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: ""))
13231323
}
13241324

1325-
guard let decimal = Decimal(string: numberString) else {
1325+
guard let decimal = jsonNumber.exactlyDecimal else {
13261326
throw DecodingError.dataCorrupted(.init(
13271327
codingPath: self.codingPath,
1328-
debugDescription: "Parsed JSON number <\(numberString)> does not fit in \(Decimal.self)."))
1328+
debugDescription: "Parsed JSON number <\(jsonNumber)> does not fit in \(Decimal.self)."))
13291329
}
13301330

13311331
return decimal
@@ -1361,18 +1361,20 @@ extension JSONDecoderImpl: Decoder {
13611361
for additionalKey: CodingKey? = nil,
13621362
as type: T.Type) throws -> T
13631363
{
1364-
if case .number(let number) = value {
1365-
guard let floatingPoint = T(number) else {
1366-
var path = self.codingPath
1367-
if let additionalKey = additionalKey {
1368-
path.append(additionalKey)
1369-
}
1370-
throw DecodingError.dataCorrupted(.init(
1371-
codingPath: path,
1372-
debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)."))
1364+
if case .number(let jsonNumber) = value {
1365+
if type == Double.self, let number = jsonNumber.exactlyDouble {
1366+
return number as! T
13731367
}
1374-
1375-
return floatingPoint
1368+
if type == Float.self, let number = jsonNumber.exactlyFloat {
1369+
return number as! T
1370+
}
1371+
var path = self.codingPath
1372+
if let additionalKey = additionalKey {
1373+
path.append(additionalKey)
1374+
}
1375+
throw DecodingError.dataCorrupted(.init(
1376+
codingPath: path,
1377+
debugDescription: "Parsed JSON number <\(jsonNumber)> does not fit in \(T.self)."))
13761378
}
13771379

13781380
if case .string(let string) = value,
@@ -1396,57 +1398,48 @@ extension JSONDecoderImpl: Decoder {
13961398
for additionalKey: CodingKey? = nil,
13971399
as type: T.Type) throws -> T
13981400
{
1399-
guard case .number(let number) = value else {
1401+
guard case .number(let jsonNumber) = value else {
14001402
throw self.createTypeMismatchError(type: T.self, for: additionalKey, value: value)
14011403
}
14021404

1403-
// this is the fast pass. Number directly convertible to Integer
1404-
if let integer = T(number) {
1405-
return integer
1405+
if type == UInt8.self, let number = jsonNumber.exactlyUInt8 {
1406+
return number as! T
14061407
}
1407-
1408-
// this is the really slow path... If the fast path has failed. For example for "34.0" as
1409-
// an integer, we try to go through NSNumber
1410-
if let nsNumber = NSNumber.fromJSONNumber(number) {
1411-
if type == UInt8.self, NSNumber(value: nsNumber.uint8Value) == nsNumber {
1412-
return nsNumber.uint8Value as! T
1413-
}
1414-
if type == Int8.self, NSNumber(value: nsNumber.int8Value) == nsNumber {
1415-
return nsNumber.uint8Value as! T
1416-
}
1417-
if type == UInt16.self, NSNumber(value: nsNumber.uint16Value) == nsNumber {
1418-
return nsNumber.uint16Value as! T
1419-
}
1420-
if type == Int16.self, NSNumber(value: nsNumber.int16Value) == nsNumber {
1421-
return nsNumber.uint16Value as! T
1422-
}
1423-
if type == UInt32.self, NSNumber(value: nsNumber.uint32Value) == nsNumber {
1424-
return nsNumber.uint32Value as! T
1425-
}
1426-
if type == Int32.self, NSNumber(value: nsNumber.int32Value) == nsNumber {
1427-
return nsNumber.uint32Value as! T
1428-
}
1429-
if type == UInt64.self, NSNumber(value: nsNumber.uint64Value) == nsNumber {
1430-
return nsNumber.uint64Value as! T
1431-
}
1432-
if type == Int64.self, NSNumber(value: nsNumber.int64Value) == nsNumber {
1433-
return nsNumber.uint64Value as! T
1434-
}
1435-
if type == UInt.self, NSNumber(value: nsNumber.uintValue) == nsNumber {
1436-
return nsNumber.uintValue as! T
1437-
}
1438-
if type == Int.self, NSNumber(value: nsNumber.uintValue) == nsNumber {
1439-
return nsNumber.intValue as! T
1440-
}
1408+
if type == Int8.self, let number = jsonNumber.exactlyInt8 {
1409+
return number as! T
14411410
}
1442-
1411+
if type == UInt16.self, let number = jsonNumber.exactlyUInt16 {
1412+
return number as! T
1413+
}
1414+
if type == Int16.self, let number = jsonNumber.exactlyInt16 {
1415+
return number as! T
1416+
}
1417+
if type == UInt32.self, let number = jsonNumber.exactlyUInt32 {
1418+
return number as! T
1419+
}
1420+
if type == Int32.self, let number = jsonNumber.exactlyInt32 {
1421+
return number as! T
1422+
}
1423+
if type == UInt64.self, let number = jsonNumber.exactlyUInt64 {
1424+
return number as! T
1425+
}
1426+
if type == Int64.self, let number = jsonNumber.exactlyInt64 {
1427+
return number as! T
1428+
}
1429+
if type == UInt.self, let number = jsonNumber.exactlyUInt {
1430+
return number as! T
1431+
}
1432+
if type == Int.self, let number = jsonNumber.exactlyInt {
1433+
return number as! T
1434+
}
1435+
14431436
var path = self.codingPath
14441437
if let additionalKey = additionalKey {
14451438
path.append(additionalKey)
14461439
}
14471440
throw DecodingError.dataCorrupted(.init(
14481441
codingPath: path,
1449-
debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)."))
1442+
debugDescription: "Parsed JSON number <\(jsonNumber)> does not fit in \(T.self)."))
14501443
}
14511444

14521445
private func createTypeMismatchError(type: Any.Type, for additionalKey: CodingKey? = nil, value: JSONValue) -> DecodingError {

0 commit comments

Comments
 (0)