-
Notifications
You must be signed in to change notification settings - Fork 1.2k
JSONSerialization: Add missing numeric types #1197
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
JSONSerialization: Add missing numeric types #1197
Conversation
- Add support for Int8/Int16/Int32/Int64, UInt8/UInt16/UInt32/UInt64, Decimal and NSDecimalNumber. - Improve serializeInteger performance to avoid overall speed regression.
- Move JSONSerialization tests into correct testfile and use Int64 to stop tests breaking on 32bit platforms. - Add tests for isValidJSONObject() to test the support of new types. - Add tests for Float, Double, Decimal and NSDecimalNumber.
Foundation/JSONSerialization.swift
Outdated
@@ -63,7 +63,9 @@ open class JSONSerialization : NSObject { | |||
} | |||
|
|||
// object is Swift.String, NSNull, Int, Bool, or UInt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete this comment, which is now wrong (and was pointless anyway)?
} | ||
|
||
writer(stringResult) | ||
let output = String(cString: Array(buffer.suffix(from: pos))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this do two copies? One to create a new Array
and another to create the String
. Can we avoid at least one of them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally I was allocating a buffer once in the JSONWriter()
and using String(bytesNoCopy:)
but even that does a copy and then there is another one when the string is appended to the JSON output.
So I decided not to do anything too complicated in this PR and just copied this from Decimal
.
In a followup PR I will try converting JSONWriter
to allocate the output buffer at the start and just have the serialise methods write directly into it and hopefully avoid any copying.
@djones6 Ran some tests against this change and confirmed that it is faster than the current method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing directly into the output buffer sounds much better - thanks.
Foundation/JSONSerialization.swift
Outdated
writer(_serializationString(for: num)) | ||
if CFNumberIsFloatType(num._cfObject) { | ||
if !num.doubleValue.isFinite { | ||
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.propertyListReadCorrupt.rawValue, userInfo: ["NSDebugDescription" : "Number cannot be infinity or NaN"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this should say "Number must be finite" now. Or is the string exactly matching Darwin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated it to emit the same message as Darwin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, just a few comments.
@swift-ci please test |
@swift-ci please test |
TestFoundation/TestJSONEncoder.swift
Outdated
let intMax:Int = Int.max | ||
let uintMin:UInt = UInt.min | ||
let uintMax:UInt = UInt.max | ||
let intMin = Int64.min |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this would be better called int64Min
now. No big deal though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable overall — thanks for going through and doing this. Some minor comments below, but the major things I think we should fix are taking out support for infinity and NaN, and writing out floating-point values in scientific notation.
Foundation/JSONSerialization.swift
Outdated
array.append(number % 10) | ||
private func serializeInteger<T: UnsignedInteger>(value: T, isNegative: Bool = false) { | ||
let maxIntLength = 22 // 20 digits in UInt64 + optional sign + trailing '\0' | ||
let ZERO: CChar = 0x30 // ASCII '0' == 0x30 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a purely stylistic standpoint, I don't think we really use all caps anywhere to indicate a constant. I think this would read more clearly as just asciiZero
(and MINUS
would read a bit better as asciiMinus
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually copied this from Decimal
but I will tidy it up.
if !dv.isFinite { | ||
let value: String | ||
if dv.isNaN { | ||
value = "NaN" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is inconsistent with NSJSONSerialization
, which does not support NaN
or infinite values. This should arguably preconditionFailure
/fatalError
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Seeing also as we claim in isValidJSONValue
that infinite numbers/NaN
are not valid, we shouldn't support them here.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually to exactly match the error messaging on Darwin, not to serialise the value. value
is used in the NSError
below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're totally right, of course — I'd misread this. 😬
|
||
func test_serialize_Float() { | ||
XCTAssertEqual(try trySerialize([-Float.leastNonzeroMagnitude, Float.leastNonzeroMagnitude]), "[-0,0]") | ||
XCTAssertEqual(try trySerialize([-Float.greatestFiniteMagnitude]), "[-340282346638529000000000000000000000000]") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value (and all floating-point values below, actually) is inconsistent with NSJSONSerialization
, which outputs floating-point numbers in scientific notation (via snprintf("%*.g", DBL_DECIMAL_DIG, ...)
). We should likely bridge that gap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did try and play around with the Float
and Double
output formats but it broke some other tests. I will see if I can find a better solution - I was actually intending to do a follow-up PR to fix up the output formatting.
Foundation/JSONSerialization.swift
Outdated
let invalid = number.doubleValue.isInfinite || number.doubleValue.isNaN | ||
return !invalid | ||
if CFNumberIsFloatType(number._cfObject) { | ||
return number.doubleValue.isFinite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line made me do a double-take for a moment because at first glance, it's hard to visually distinguish between number.doubleValue.isFinite
and number.doubleValue.isInfinite
. Do you mind just calling this out with a comment saying something like "All floating-point numbers except for NaN and infinity are valid." (or similar)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I will actually put it back to how it was but store the doubleValue
in a local since getting the doubleValue
twice involved into 2 calls into CFNumber
so was a significant speedup.
XCTAssertTrue(JSONSerialization.isValidJSONObject([Decimal(123), Decimal(Double.leastNonzeroMagnitude)])) | ||
|
||
XCTAssertFalse(JSONSerialization.isValidJSONObject(Float.nan)) | ||
XCTAssertFalse(JSONSerialization.isValidJSONObject(Float.infinity)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness' sake, it's worth also asserting this on negative infinity (below as well).
I have fixed all of the outstanding comments expect for the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM; thanks!
@swift-ci Please test |
@itaiferber Looks like the test worked on CI, is this ok to merge now? |
@spevans Which builder did this build on? I don't see any GitHub CI state for it (likely a GitHub issue) |
Decimal and NSDecimalNumber.
breaking on 32bit platforms.
This PR does not address the output format of
Float
andDouble
, which are not currently the same asFoundation
. That will be addressed in a follow-up PR