12

Take the following piece of code:

NSError *error;
NSString *myJSONString = @"{ \"foo\" : 0.1}";
NSData *jsonData = [myJSONString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *results = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];

My question is, is results[@"foo"] an NSDecimalNumber, or something with finite binary precision like a double or float? Basically, I have an application that requires the lossless accuracy that comes with an NSDecimalNumber, and need to ensure that the JSON deserialization doesn't result in rounding because of doubles/floats etcetera.

E.g. if it was interpreted as a float, I'd run into problems like this with precision:

float baz = 0.1;
NSLog(@"baz: %.20f", baz);
// prints baz: 0.10000000149011611938

I've tried interpreting foo as an NSDecimalNumber and printing the result:

NSDecimalNumber *fooAsDecimal = results[@"foo"];
NSLog(@"fooAsDecimal: %@", [fooAsDecimal stringValue]);
// prints fooAsDecimal: 0.1

But then I found that calling stringValue on an NSDecimalNumber doesn't print all significant digits anyway, e.g...

NSDecimalNumber *barDecimal = [NSDecimalNumber decimalNumberWithString:@"0.1000000000000000000000000000000000000000000011"];
NSLog(@"barDecimal: %@", barDecimal);
// prints barDecimal: 0.1

...so printing fooAsDecimal doesn't tell me whether results[@"foo"] was at some point rounded to finite precision by the JSON parser or not.

To be clear, I realise I could use a string rather than a number in the JSON representation to store the value of foo, i.e. "0.1" instead of 0.1, and then use [NSDecimalNumber decimalNumberWithString:results[@"foo"]]. But, what I'm interested in is how the NSJSONSerialization class deserializes JSON numbers, so I know whether this is really necessary or not.

Bryce Thomas
  • 10,479
  • 26
  • 77
  • 126

4 Answers4

7

NSJSONSerialization (and JSONSerialization in Swift) follow the general pattern:

  1. If a number has only an integer part (no decimal or exponent), attempt to parse it as a long long. If that doesn't overflow, return an NSNumber with long long.
  2. Attempt to parse a double with strtod_l. If it doesn't overflow, return an NSNumber with double.
  3. In all other cases, attempt to use NSDecimalNumber which supports a much larger range of values, specifically a mantissa up to 38 digits and exponent between -128...127.

If you look at other examples people have posted you can see that when the value exceeds the range or precision of a double you get an NSDecimalNumber back.

russbishop
  • 16,587
  • 7
  • 61
  • 74
  • where does this information comes from? Is this a quote? I'm struggling now with JSON value `0.115` trying to compare or convert to NSDecimalNumber – it fails because mantissa is 1 bit different after conversion! – ReDetection Mar 09 '21 at 15:25
4

The short answer is that you should not serialize to JSON if you require NSDecimalNumber levels of precision. JSON has only one number format: double, which has inferior precision to NSDecimalNumber.

The long answer, which is of academic interest only, because the short answer is also the right answer, is "Not necessarily." NSJSONSerialization does sometimes deserialize as NSDecimalNumber, but it is undocumented, and I have not determined, what the set of circumstances under which it does is. For instance:

    BOOL boolYes = YES;
    int16_t int16 = 12345;
    int32_t int32 = 2134567890;
    uint32_t uint32 = 3124141341;
    unsigned long long ull = 312414134131241413ull;
    double dlrep = 1.5;
    double dlmayrep = 1.1234567891011127;
    float fl = 3124134134678.13;
    double dl = 13421331.72348729 * 1000000000000000000000000000000000000000000000000000.0;
    long long negLong = -632414314135135234;
    unsigned long long unrepresentable = 10765432100123456789ull;

    dict[@"bool"] = @(boolYes);
    dict[@"int16"] = @(int16);
    dict[@"int32"] = @(int32);
    dict[@"dlrep"] = @(dlrep);
    dict[@"dlmayrep"] = @(dlmayrep);
    dict[@"fl"] = @(fl);
    dict[@"dl"] = @(dl);
    dict[@"uint32"] = @(uint32);
    dict[@"ull"] = @(ull);
    dict[@"negLong"] = @(negLong);
    dict[@"unrepresentable"] = @(unrepresentable);

    NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];

    NSDictionary *dict_back = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];

and in the debugger:

(lldb) po [dict_back[@"bool"] class]
__NSCFBoolean
(lldb) po [dict_back[@"int16"] class]
__NSCFNumber
(lldb) po [dict_back[@"int32"] class]
__NSCFNumber
(lldb) po [dict_back[@"ull"] class]
__NSCFNumber
(lldb) po [dict_back[@"fl"] class]
NSDecimalNumber
(lldb) po [dict_back[@"dl"] class]
NSDecimalNumber
(lldb) po [dict_back[@"dlrep"] class]
__NSCFNumber
(lldb) po [dict_back[@"dlmayrep"] class]
__NSCFNumber
(lldb) po [dict_back[@"negLong"] class]
__NSCFNumber
(lldb) po [dict_back[@"unrepresentable"] class]
NSDecimalNumber

So make of that what you will. You should definitely not assume that if you serialize an NSDecimalNumber to JSON that you will get an NSDecimalNumber back out.

But, again, you should not store NSDecimalNumbers in JSON.

icodestuff
  • 340
  • 1
  • 11
  • 6
    This is incorrect. The JSON format does not care about float or double precision. It's the JSON parser that loses the precision. – Sulthan Mar 25 '16 at 12:59
  • @icodestuff: if I run your code using `double dl = 8.92918e-128;`, `dict_back` is `nil`. Have you any idea if there is a way to get JSON deserialisation to work for such values using `NSJSONSerialization` or other libraries? – sergio Jan 03 '17 at 08:55
  • As Sulthan said, JSON does not care about precision. If Apple's API follows the specs, it should use NSDecimalNumber. – xtravar Jan 21 '17 at 05:08
  • If you check my comment, it does use `NSDecimalNumber` which supports a larger range but different precision characteristics. – russbishop Jan 21 '17 at 05:58
2

I had the same problem, except I'm using Swift 3. I made a patched version of the JSONSerialization class that parses all numbers as Decimal's. It can only parse/deserialize JSON, but does not have any serialization code. It's based on Apple's open source re-implementation of Foundation in Swift.

hashemi
  • 2,608
  • 1
  • 25
  • 31
1

To answer the question in the title: No, it doesn't, it creates NSNumber objects. You can easily test this:

NSArray *a = @[[NSDecimalNumber decimalNumberWithString:@"0.1"]];
NSData *data = [NSJSONSerialization dataWithJSONObject:a options:0 error:NULL];
a = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
NSLog(@"%@", [a[0] class]);

will print __NSCFNumber.

You can convert that NSNumber object to an NSDecimalNumber with [NSDecimalNumber decimalNumberWithDecimal:[number decimalValue]], but according to the docs for decimalValue

The value returned isn’t guaranteed to be exact for float and double values.

Sebastian
  • 7,670
  • 5
  • 38
  • 50
  • NSDecimalNumber is a subclass of NSNumber. If you use a value that does not fit into a double, then yes, it will create an NSDecimalNumber. – fishinear Sep 15 '18 at 15:35