Skip to content

JSONSerialization: Improve number parsing for JSON #2980

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion Foundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,15 @@
B910957B1EEF237800A71930 /* NSString-UTF16-BE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B91095791EEF237800A71930 /* NSString-UTF16-BE-data.txt */; };
B91161AA2429860900BD2907 /* DataURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91161A82429857D00BD2907 /* DataURLProtocol.swift */; };
B91161AD242A363900BD2907 /* TestDataURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91161AB242A350D00BD2907 /* TestDataURLProtocol.swift */; };
B9292465258E75DD00E24DA5 /* JSONNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9292464258E75DD00E24DA5 /* JSONNumber.swift */; };
B929246F258E772B00E24DA5 /* CMakeLists.txt in Resources */ = {isa = PBXBuildFile; fileRef = B929246E258E772B00E24DA5 /* CMakeLists.txt */; };
B933A79E1F3055F700FE6846 /* NSString-UTF32-BE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B933A79C1F3055F600FE6846 /* NSString-UTF32-BE-data.txt */; };
B933A79F1F3055F700FE6846 /* NSString-UTF32-LE-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = B933A79D1F3055F600FE6846 /* NSString-UTF32-LE-data.txt */; };
B940492D223B146800FB4384 /* TestProgressFraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940492C223B146800FB4384 /* TestProgressFraction.swift */; };
B94B063C23FDE2BD00B244E8 /* SwiftFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5D885D1BBC938800234F36 /* SwiftFoundation.framework */; };
B951B5EC1F4E2A2000D8B332 /* TestNSLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B951B5EB1F4E2A2000D8B332 /* TestNSLock.swift */; };
B959016E25970BE300CACAE3 /* TestJSONNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = B959016D25970BE300CACAE3 /* TestJSONNumber.swift */; };
B95901922597102000CACAE3 /* CMakeLists.txt in Resources */ = {isa = PBXBuildFile; fileRef = B95901912597102000CACAE3 /* CMakeLists.txt */; };
B95FC97622B84B0A005DEA0A /* TestNSSortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 152EF3932283457B001E1269 /* TestNSSortDescriptor.swift */; };
B96C10F625BA1EFD00985A32 /* NSURLComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96C10F525BA1EFD00985A32 /* NSURLComponents.swift */; };
B96C110025BA20A600985A32 /* NSURLQueryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96C10FF25BA20A600985A32 /* NSURLQueryItem.swift */; };
Expand Down Expand Up @@ -1109,10 +1113,14 @@
B91095791EEF237800A71930 /* NSString-UTF16-BE-data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "NSString-UTF16-BE-data.txt"; sourceTree = "<group>"; };
B91161A82429857D00BD2907 /* DataURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataURLProtocol.swift; sourceTree = "<group>"; };
B91161AB242A350D00BD2907 /* TestDataURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataURLProtocol.swift; sourceTree = "<group>"; };
B9292464258E75DD00E24DA5 /* JSONNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONNumber.swift; sourceTree = "<group>"; };
B929246E258E772B00E24DA5 /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = "<group>"; };
B933A79C1F3055F600FE6846 /* NSString-UTF32-BE-data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NSString-UTF32-BE-data.txt"; sourceTree = "<group>"; };
B933A79D1F3055F600FE6846 /* NSString-UTF32-LE-data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "NSString-UTF32-LE-data.txt"; sourceTree = "<group>"; };
B940492C223B146800FB4384 /* TestProgressFraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestProgressFraction.swift; sourceTree = "<group>"; };
B951B5EB1F4E2A2000D8B332 /* TestNSLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSLock.swift; sourceTree = "<group>"; };
B959016D25970BE300CACAE3 /* TestJSONNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestJSONNumber.swift; sourceTree = "<group>"; };
B95901912597102000CACAE3 /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = "<group>"; };
B95FC97222AF0050005DEA0A /* SwiftXCTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftXCTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B95FC97422AF051B005DEA0A /* xcode-build.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "xcode-build.sh"; sourceTree = "<group>"; };
B96C10F525BA1EFD00985A32 /* NSURLComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSURLComponents.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1777,6 +1785,7 @@
F023071023F0976B0023DBEC /* Foundation */ = {
isa = PBXGroup;
children = (
B95901912597102000CACAE3 /* CMakeLists.txt */,
155D3BBB22401D1100B0D38E /* FixtureValues.swift */,
616068F2225DE5C2004FCC54 /* FTPServer.swift */,
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */,
Expand Down Expand Up @@ -1819,6 +1828,7 @@
EA66F63E1BF1619600136161 /* TestIndexSet.swift */,
63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */,
3EA9D66F1EF0532D00B362D6 /* TestJSONEncoder.swift */,
B959016D25970BE300CACAE3 /* TestJSONNumber.swift */,
5EB6A15C1C188FC40037DCB8 /* TestJSONSerialization.swift */,
BD8042151E09857800487EB8 /* TestLengthFormatter.swift */,
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */,
Expand Down Expand Up @@ -2042,6 +2052,7 @@
F023072323F0A6E50023DBEC /* Foundation */ = {
isa = PBXGroup;
children = (
B929246E258E772B00E24DA5 /* CMakeLists.txt */,
F023072523F0B4890023DBEC /* Headers */,
F023072423F0B4140023DBEC /* Resources */,
EADE0B4D1BD09E0800C49C64 /* AffineTransform.swift */,
Expand Down Expand Up @@ -2081,6 +2092,7 @@
5B8BA1611D0B773A00938C27 /* IndexSet.swift */,
63DCE9D11EAA430100E9CB02 /* ISO8601DateFormatter.swift */,
3EDCE5091EF04D8100C2EC04 /* JSONEncoder.swift */,
B9292464258E75DD00E24DA5 /* JSONNumber.swift */,
EADE0B641BD15DFF00C49C64 /* JSONSerialization.swift */,
49D55FA025E84FE5007BD3B3 /* JSONSerialization+Parser.swift */,
EADE0B661BD15DFF00C49C64 /* LengthFormatter.swift */,
Expand All @@ -2100,6 +2112,7 @@
5BDC3FCD1BCF17D300ED97BB /* NSCFDictionary.swift */,
5BDC3FCF1BCF17E600ED97BB /* NSCFSet.swift */,
5BDC3FCB1BCF177E00ED97BB /* NSCFString.swift */,
15CA750924F8336A007DF6C1 /* NSCFTypeShims.swift */,
5BDC3F311BCC5DCB00ED97BB /* NSCharacterSet.swift */,
5BDC3F321BCC5DCB00ED97BB /* NSCoder.swift */,
EADE0B551BD15DFF00C49C64 /* NSComparisonPredicate.swift */,
Expand All @@ -2122,7 +2135,6 @@
D3BCEB9F1C2F6DDB00295652 /* NSKeyedCoderOldStyleArray.swift */,
D39A14001C2D6E0A00295652 /* NSKeyedUnarchiver.swift */,
5BDC3F3B1BCC5DCB00ED97BB /* NSLocale.swift */,
15CA750924F8336A007DF6C1 /* NSCFTypeShims.swift */,
5BDC3F3C1BCC5DCB00ED97BB /* NSLock.swift */,
D3BCEB9D1C2EDED800295652 /* NSLog.swift */,
5BECBA391D1CAE9A00B39B1F /* NSMeasurement.swift */,
Expand Down Expand Up @@ -2713,6 +2725,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B929246F258E772B00E24DA5 /* CMakeLists.txt in Resources */,
F023072623F0B4890023DBEC /* Headers in Resources */,
B983E32C23F0C69600D9C402 /* Docs in Resources */,
B983E32E23F0C6E200D9C402 /* CONTRIBUTING.md in Resources */,
Expand Down Expand Up @@ -2743,6 +2756,7 @@
D3A597F71C3415CC00295652 /* NSKeyedUnarchiver-ArrayTest.plist in Resources */,
CE19A88C1C23AA2300B4CB6A /* NSStringTestData.txt in Resources */,
E1A03F361C4828650023AF4D /* PropertyList-1.0.dtd in Resources */,
B95901922597102000CACAE3 /* CMakeLists.txt in Resources */,
E1A3726F1C31EBFB0023AF4D /* NSXMLDocumentTestData.xml in Resources */,
E1A03F381C482C730023AF4D /* NSXMLDTDTestData.xml in Resources */,
D3A598041C349E6A00295652 /* NSKeyedUnarchiver-OrderedSetTest.plist in Resources */,
Expand Down Expand Up @@ -2888,6 +2902,7 @@
5BF7AEC01BCD51F9008F214A /* NSUUID.swift in Sources */,
5BF7AEB01BCD51F9008F214A /* NSLocale.swift in Sources */,
EADE0BA31BD15E0000C49C64 /* NSKeyedArchiver.swift in Sources */,
B9292465258E75DD00E24DA5 /* JSONNumber.swift in Sources */,
5BF7AEAD1BCD51F9008F214A /* NSError.swift in Sources */,
EADE0BB61BD15E0000C49C64 /* NSSortDescriptor.swift in Sources */,
5BF7AEA41BCD51F9008F214A /* Bundle.swift in Sources */,
Expand Down Expand Up @@ -3104,6 +3119,7 @@
90E645DF1E4C89A400D0D47C /* TestNSCache.swift in Sources */,
5B13B34A1C582D4C00651CE2 /* TestURL.swift in Sources */,
EA54A6FB1DB16D53009E0809 /* TestObjCRuntime.swift in Sources */,
B959016E25970BE300CACAE3 /* TestJSONNumber.swift in Sources */,
BB3D7558208A1E500085CFDC /* Imports.swift in Sources */,
5B13B34D1C582D4C00651CE2 /* TestNSUUID.swift in Sources */,
15F10CDC218909BF00D88114 /* TestNSCalendar.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Sources/Foundation/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ add_library(Foundation
IndexSet.swift
ISO8601DateFormatter.swift
JSONEncoder.swift
JSONNumber.swift
JSONSerialization.swift
JSONSerialization+Parser.swift
LengthFormatter.swift
Expand Down
125 changes: 61 additions & 64 deletions Sources/Foundation/JSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ extension JSONEncoderImpl: _SpecialTreatmentEncoder {
case let url as URL:
return .string(url.absoluteString)
case let decimal as Decimal:
return .number(decimal.description)
return .outputNumber(decimal.description)
case let object as [String: Encodable]: // this emits a warning, but it works perfectly
return try self.wrapObject(object, for: nil)
case let date as Date:
Expand Down Expand Up @@ -507,7 +507,7 @@ extension _SpecialTreatmentEncoder {
if string.hasSuffix(".0") {
string.removeLast(2)
}
return .number(string)
return .outputNumber(string)
}

fileprivate func wrapEncodable<E: Encodable>(_ encodable: E, for additionalKey: CodingKey?) throws -> JSONValue? {
Expand All @@ -519,7 +519,7 @@ extension _SpecialTreatmentEncoder {
case let url as URL:
return .string(url.absoluteString)
case let decimal as Decimal:
return .number(decimal.description)
return .outputNumber(decimal.description)
case let object as [String: Encodable]:
return try self.wrapObject(object, for: additionalKey)
default:
Expand All @@ -537,10 +537,10 @@ extension _SpecialTreatmentEncoder {
return encoder.value ?? .null

case .secondsSince1970:
return .number(date.timeIntervalSince1970.description)
return .outputNumber(date.timeIntervalSince1970.description)

case .millisecondsSince1970:
return .number((date.timeIntervalSince1970 * 1000).description)
return .outputNumber((date.timeIntervalSince1970 * 1000).description)

case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
Expand Down Expand Up @@ -751,7 +751,7 @@ extension JSONKeyedEncodingContainer {
}

@inline(__always) private mutating func encodeFixedWidthInteger<N: FixedWidthInteger>(_ value: N, key: CodingKey) throws {
self.object.set(.number(value.description), for: key.stringValue)
self.object.set(.outputNumber(value.description), for: key.stringValue)
}
}

Expand Down Expand Up @@ -872,7 +872,7 @@ private struct JSONUnkeyedEncodingContainer: UnkeyedEncodingContainer, _SpecialT

extension JSONUnkeyedEncodingContainer {
@inline(__always) private mutating func encodeFixedWidthInteger<N: FixedWidthInteger>(_ value: N) throws {
self.array.append(.number(value.description))
self.array.append(.outputNumber(value.description))
}

@inline(__always) private mutating func encodeFloatingPoint<F: FloatingPoint & CustomStringConvertible>(_ float: F) throws {
Expand Down Expand Up @@ -971,7 +971,7 @@ private struct JSONSingleValueEncodingContainer: SingleValueEncodingContainer, _
extension JSONSingleValueEncodingContainer {
@inline(__always) private mutating func encodeFixedWidthInteger<N: FixedWidthInteger>(_ value: N) throws {
self.preconditionCanEncodeNewValue()
self.impl.singleValue = .number(value.description)
self.impl.singleValue = .outputNumber(value.description)
}

@inline(__always) private mutating func encodeFloatingPoint<F: FloatingPoint & CustomStringConvertible>(_ float: F) throws {
Expand Down Expand Up @@ -1011,7 +1011,9 @@ extension JSONValue {
bytes.append(contentsOf: [UInt8]._false)
case .string(let string):
self.encodeString(string, to: &bytes)
case .number(let string):
case .inputNumber(let jsonNumber):
bytes.append(contentsOf: jsonNumber.description.utf8)
case .outputNumber(let string):
bytes.append(contentsOf: string.utf8)
case .array(let array):
var iterator = array.makeIterator()
Expand Down Expand Up @@ -1070,7 +1072,9 @@ extension JSONValue {
bytes.append(contentsOf: [UInt8]._false)
case .string(let string):
self.encodeString(string, to: &bytes)
case .number(let string):
case .inputNumber(let jsonNumber):
bytes.append(contentsOf: jsonNumber.description.utf8)
case .outputNumber(let string):
bytes.append(contentsOf: string.utf8)
case .array(let array):
var iterator = array.makeIterator()
Expand Down Expand Up @@ -1529,14 +1533,14 @@ extension JSONDecoderImpl: Decoder {
}

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

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

return decimal
Expand Down Expand Up @@ -1572,18 +1576,20 @@ extension JSONDecoderImpl: Decoder {
for additionalKey: CodingKey? = nil,
as type: T.Type) throws -> T
{
if case .number(let number) = value {
guard let floatingPoint = T(number) else {
var path = self.codingPath
if let additionalKey = additionalKey {
path.append(additionalKey)
}
throw DecodingError.dataCorrupted(.init(
codingPath: path,
debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)."))
if case .inputNumber(let jsonNumber) = value {
if type == Double.self, let number = jsonNumber.exactlyDouble {
return number as! T
}

return floatingPoint
if type == Float.self, let number = jsonNumber.exactlyFloat {
return number as! T
}
var path = self.codingPath
if let additionalKey = additionalKey {
path.append(additionalKey)
}
throw DecodingError.dataCorrupted(.init(
codingPath: path,
debugDescription: "Parsed JSON number <\(jsonNumber)> does not fit in \(T.self)."))
}

if case .string(let string) = value,
Expand All @@ -1607,57 +1613,48 @@ extension JSONDecoderImpl: Decoder {
for additionalKey: CodingKey? = nil,
as type: T.Type) throws -> T
{
guard case .number(let number) = value else {
guard case .inputNumber(let jsonNumber) = value else {
throw self.createTypeMismatchError(type: T.self, for: additionalKey, value: value)
}

// this is the fast pass. Number directly convertible to Integer
if let integer = T(number) {
return integer
if type == UInt8.self, let number = jsonNumber.exactlyUInt8 {
return number as! T
}

// this is the really slow path... If the fast path has failed. For example for "34.0" as
// an integer, we try to go through NSNumber
if let nsNumber = NSNumber.fromJSONNumber(number) {
if type == UInt8.self, NSNumber(value: nsNumber.uint8Value) == nsNumber {
return nsNumber.uint8Value as! T
}
if type == Int8.self, NSNumber(value: nsNumber.int8Value) == nsNumber {
return nsNumber.uint8Value as! T
}
if type == UInt16.self, NSNumber(value: nsNumber.uint16Value) == nsNumber {
return nsNumber.uint16Value as! T
}
if type == Int16.self, NSNumber(value: nsNumber.int16Value) == nsNumber {
return nsNumber.uint16Value as! T
}
if type == UInt32.self, NSNumber(value: nsNumber.uint32Value) == nsNumber {
return nsNumber.uint32Value as! T
}
if type == Int32.self, NSNumber(value: nsNumber.int32Value) == nsNumber {
return nsNumber.uint32Value as! T
}
if type == UInt64.self, NSNumber(value: nsNumber.uint64Value) == nsNumber {
return nsNumber.uint64Value as! T
}
if type == Int64.self, NSNumber(value: nsNumber.int64Value) == nsNumber {
return nsNumber.uint64Value as! T
}
if type == UInt.self, NSNumber(value: nsNumber.uintValue) == nsNumber {
return nsNumber.uintValue as! T
}
if type == Int.self, NSNumber(value: nsNumber.uintValue) == nsNumber {
return nsNumber.intValue as! T
}
if type == Int8.self, let number = jsonNumber.exactlyInt8 {
return number as! T
}

if type == UInt16.self, let number = jsonNumber.exactlyUInt16 {
return number as! T
}
if type == Int16.self, let number = jsonNumber.exactlyInt16 {
return number as! T
}
if type == UInt32.self, let number = jsonNumber.exactlyUInt32 {
return number as! T
}
if type == Int32.self, let number = jsonNumber.exactlyInt32 {
return number as! T
}
if type == UInt64.self, let number = jsonNumber.exactlyUInt64 {
return number as! T
}
if type == Int64.self, let number = jsonNumber.exactlyInt64 {
return number as! T
}
if type == UInt.self, let number = jsonNumber.exactlyUInt {
return number as! T
}
if type == Int.self, let number = jsonNumber.exactlyInt {
return number as! T
}

var path = self.codingPath
if let additionalKey = additionalKey {
path.append(additionalKey)
}
throw DecodingError.dataCorrupted(.init(
codingPath: path,
debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)."))
debugDescription: "Parsed JSON number <\(jsonNumber)> does not fit in \(T.self)."))
}

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