I'll add that for Haskell, the library everyone uses for JSON parses numbers into Scientific types with almost unlimited size and precision. I say almost unlimited because they use a decimal coefficient-and-exponent representation where the exponent is a 64-bit integer.<p>The documentation is quite paranoid that if you are dealing with untrusted inputs, you could parse two JSON numbers from the untrusted source fine and then performing an addition on them could cause your memory to fill up. Exciting new DoS vector.<p>Of course in practice people end up parsing them into custom types with 64-bit integers, so this is only a problem if you are manipulating JSON directly which is very rare in Haskell.
One of the first Ajax projects I worked on was multi tenant, and someone decided to solve the industrial espionage problem by using random 64 bit identifiers for all records in the system. You have about a .1% chance of generating an ID that gets truncated in JavaScript, which is just enough that you might make it past MVP before anyone figures out it’s broken, and that’s exactly what happened to us.<p>So we had to go through all the code adding quotes to all the ID fields. That was a giant pain in my ass.
Long story short: don't use JSON numbers to represent money or monetary rates. Always use decimals encoded as string. It's surprising how many APIs fall short of this basic bar.
I tend to end up encoding everything as an integer (multiply by 1000, 10000 etc) and then turn it back into a float/decimal on decode. For instance if I am building a system dealing with dollar amounts I will store cent amounts everywhere, communicate cent amounts over the wire, etc. then treat it as a presentation concern to render it as a dollar amount.
My opinion is that a safe approach is to use either 52-bit integer number or 64-bit floating number to keep JavaScript compatibility. JavaScript is too important and at the same time, the errors are too terrific (JS will silently round to the nearest 52-bit integer number which could lead to various exploits) to skip on that. If you need anything else, just use strings.
I think the description for Go is inaccurate/incomplete. You can call this function to instruct the decoder to leave numbers in unparsed string form:<p><a href="https://pkg.go.dev/encoding/json#Decoder.UseNumber" rel="nofollow">https://pkg.go.dev/encoding/json#Decoder.UseNumber</a><p>That allows you to capture/forward numbers without any loss of precision.
Other values one could test for:<p>- <i>“+1”</i> (not a valid number, according to ECMA-404 and RFC-8259)<p>- <i>“+0”</i> (also not a valid number, but trickier than <i>“+1”</i> because IEEE floats have <i>“+0”</i> and <i>“-0”</i>)<p>- <i>“070”</i> (not a valid number, but may get parsed as octal 56)<p>- <i>“1.”</i> (not a valid number in json)<p>- <i>“.1”</i> (not a valid number in json)<p>- <i>“0E-0”</i> (a valid number in json)<p>There probably are others.
> I-JSON messages SHOULD NOT include numbers that express greater magnitude or precision than an IEEE 754 double precision number provides<p>I'm confused by this.<p>What is the precision of 0.1, relative to IEEE 754?<p>If I read it correctly, that statement is saying:<p><pre><code> json_number_precision(json_number) <= ieee_754_precision
</code></pre>
^ How do I calculate these values?
First thing that I check before using a JSON parser library is that if it lets me to get the number as a string and let me do my own conversion. Libraries that try to treat the number as double or bring in a large bigint/decimal library gets usually pass from me.
When I wrote my jsonptr tool a few years ago, I noticed that some JSON libraries (in both C++ and Rust) don't even do "parse a string of decimal digits as a float64" properly. I don't mean that in the "0.3 isn't exactly representable; have 0.30000000000000004 instead" sense.<p>I mean that rapidjson (C++) parsed the string "0.99999999999999999" as the number 1.0000000000000003. Apart from just looking weird, it's a different float64 bit-pattern: 0x3FF0000000000000 vs 0x3FF0000000000001.<p>Similarly, serde-json (Rust) parsed "122.416294033786585" as 122.4162940337866. This isn't as obvious a difference, but the bit-patterns differ by one: 0x405E9AA48FBB2888 vs 0x405E9AA48FBB2889. Serde-json does have an "float_roundtrip" feature flag, but it's opt-in, not enabled by default.<p>For details, look for "rapidjson issue #1773" and "serde_json issue #707" at <a href="https://nigeltao.github.io/blog/2020/jsonptr.html" rel="nofollow">https://nigeltao.github.io/blog/2020/jsonptr.html</a>
I think the thing folk miss is when there’s an error like divide by zero, or the calculation would return NaN. I feel like this is the main gap/concern with using JSON and it seems to be rarely discussed.
I like to think of floating point values as noisy analog voltages, with the extra propery that they can store small integers perfectly, and they can be copied within code but not round trip serialized and deserializer without noise.<p>They're not really noisy, but if an application would work with some random noise added, it will probably work with floats, and if it wouldn't work with noise added, it's probably easier to just not use floats and expext people to reason about IEEE details, while risking subtle bugs if different float representations get mixed.<p>Of course I'm not doing a lot of high performance algorithms, I would imagine in some applications you really do need to reason about floats.
I think that good decision regarding numbers in API (as it was made in my project) is to put meaningful decimal numbers into string and let them be handled by exact decimal calculation framework, e.g. BigDecimal in Java etc.
Does anyone have any idea why Crockford decided that at least one digit is required after the decimal point, as opposed to JavaScript which has zero or more?
Since JSON is so widely used it should be modified to support more types - Mongo DB's Extended JSON supports all the BSON (Binary) types:<p><pre><code> Array
Binary
Date
Decimal128
Document
Double
Int32
Int64
MaxKey
MinKey
ObjectId
Regular Expression
Timestamp
</code></pre>
<a href="https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/" rel="nofollow">https://www.mongodb.com/docs/manual/reference/mongodb-extend...</a>
"ID numbers start from 2^53 and are allocated sequentially including odd numbers that are not compatible with "double" types. Please ensure you are reading this value as a 64-bit integer."
A little off topic, but fun to see that someone else has adopted that magical CSS theme! (<a href="https://css.winterveil.net/" rel="nofollow">https://css.winterveil.net/</a>)