Skip to content

Conversation

@mathiasbynens
Copy link
Member

Although JSON supports -0 per ECMA-404 and JSON.parse('-0') returns -0, JSON.stringify(-0) currently loses the sign, outputting just '0'.

This patch makes JSON.stringify(-0) return '-0' instead, so that the following holds:

Object.is(JSON.parse('-0'), -0);
// → true (this is already the case, even without this patch)
JSON.stringify(-0);
// → '-0'
Object.is(JSON.parse(JSON.stringify(-0)), -0);
// → true

Results in current JavaScript engines:

$ eshost -se 'Object.is(JSON.parse("-0"), -0)'
#### Chakra, JavaScriptCore, SpiderMonkey, V8, V8 --harmony
true

#### XS
false

$ eshost -se 'JSON.stringify(-0)'
#### Chakra, JavaScriptCore, SpiderMonkey, V8, V8 --harmony, XS
0

$ eshost -se 'Object.is(JSON.parse(JSON.stringify(-0)), -0)'
#### Chakra
true

#### JavaScriptCore, SpiderMonkey, V8, V8 --harmony, XS
false

@mathiasbynens mathiasbynens force-pushed the json-stringify-negative-zero branch from 7c76421 to 897f6c6 Compare March 5, 2019 07:59
@mathiasbynens mathiasbynens added normative change Affects behavior required to correctly evaluate some ECMAScript source text needs consensus This needs committee consensus before it can be eligible to be merged. needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 labels Mar 5, 2019
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change LGTM, but I think we'll also want web compat data, to ensure nobody's relying on this behavior.

@ljharb ljharb added the needs data This PR needs more information; such as web compatibility data, “web reality” (what all engines do)… label Mar 5, 2019
@ljharb ljharb changed the title [Normative] Make JSON.stringify(-0) preserve the sign Normative: Make JSON.stringify(-0) preserve the sign Mar 5, 2019
@ljharb ljharb requested review from a team and zenparsing March 5, 2019 08:03
Although JSON supports -0 per ECMA-404 and JSON.parse('-0') returns -0, JSON.stringify(-0) currently loses the sign, outputting just '0'.

This patch makes JSON.stringify(-0) return '-0' instead, so that the following holds:

    Object.is(JSON.parse('-0'), -0);
    // → true (this is already the case, even without this patch)
    JSON.stringify(-0);
    // → '-0'
    Object.is(JSON.parse(JSON.stringify(-0)), -0);
    // → true

Results in current JavaScript engines:

    $ eshost -se 'Object.is(JSON.parse("-0"), -0)'
    #### Chakra, JavaScriptCore, SpiderMonkey, V8
    true

    #### XS
    false

    $ eshost -se 'JSON.stringify(-0)'
    #### Chakra, JavaScriptCore, SpiderMonkey, V8, XS
    0

    $ eshost -se 'Object.is(JSON.parse(JSON.stringify(-0)), -0)'
    #### Chakra
    true

    #### JavaScriptCore, SpiderMonkey, V8, XS
    false
@mathiasbynens mathiasbynens force-pushed the json-stringify-negative-zero branch from 897f6c6 to 69be071 Compare March 5, 2019 08:41
@claudepache
Copy link
Contributor

Under the following assumption – which is reasonable in many situations, and for which the relevant subset of Number values is expected to represent exactly the corresponding mathematical abstract concept of ”integer”:

  • Number.isSafeInteger(a) and Number.isSafeInteger(b) are both true,

I expect that a == b holds iff the values of a and b are indistinguishable for most purposes (i.e., always, except when I specifically ask for that distinction as in Object.is(a,b), or when I perform possibly ambiguous operations as in 1/a). That includes:

  • String(a) == String(b)
  • a.toFixed(0) == b.toFixed(0)
  • (new Set([a, b])).size == 1

and, you guess it:

  • JSON.stringify(a) == JSON.stringify(b)

More generally, the distinction between +0 and -0 is primarily an artefact of the internal representation of numbers; the distinction between the two values is irrelevant for most purposes, and therefore should not surface in situations where you possibly don’t expect it.

@mathiasbynens
Copy link
Member Author

More generally, the distinction between +0 and -0 is primarily an artifact of the internal representation of numbers; the distinction between the two values is irrelevant for most purposes, and therefore should not surface in situations where you possibly don’t expect it.

JSON already surfaces it, though; it's just that JSON.stringify() doesn't currently match that decision.

@claudepache
Copy link
Contributor

JSON already surfaces it, though; it's just that JSON.stringify() doesn't currently match that decision.

Note that the case of JSON.parse("-0") producing -0 is not the same thing, because it does not add a distinction where I don’t expect it. Indeed, I do have JSON.parse("-0") == JSON.parse("0"), just as I have, e.g., JSON.parse("1.0") == JSON.parse("1"). (Ditto for Number.parseFloat(), etc.)

@erights
Copy link

erights commented Mar 5, 2019

I object to this PR and agree with @claudepache . This violates the original intent of the JSON.stringify design. It would at least need general consensus, and I doubt it would gain mine.

@mathiasbynens
Copy link
Member Author

Can you elaborate on the original intent of the JSON.stringify design, @erights?

@claudepache
Copy link
Contributor

Note that this discrepancy is not an innovation of JSON.{parse,stringify}(). Already as of ES3, number-to-string conversion methods and functions
(Number.prototype.{toExponential,toFixed,toPrecision,toString}(), String()) conflate +0 and -0, whereas string-to-number conversion functions (Number(), parseFloat(), parseInt()), keep the distinction between "0" and "-0".

@allenwb
Copy link
Member

allenwb commented Mar 6, 2019

Also, note that the ES5 JSON.stringify specification was derived from upon Crockford's json2.js package which used String() to produce the output for finite values of typeof "number".

@waldemarhorwat
Copy link

I agree with @erights here. We intentionally made (-0).toString() return "0" instead of "-0", and the same logic applies here.

@ljharb
Copy link
Member

ljharb commented Mar 8, 2019

@waldemarhorwat why was that decision originally made for number toString?

@mathiasbynens
Copy link
Member Author

I'll happily withdraw this PR since it is apparently working as intended. However, it'd be good to clearly document the history in this thread. I'll keep the PR open for now until someone provides that context.

@erights
Copy link

erights commented Mar 11, 2019

My sense of the rationale, without implying that all this was discussed explicitly, nor that there was general consensus on this rationale. The committee doesn't ask for consensus on rationale, just conclusions, on which we did agree.


A program that does not otherwise need to care about the difference between -0 and 0 should, as much as possible, be able to ignore this difference while remaining correct. After all, -0 and 0 denote the same real number. This is why (over my objections at the time), Map and Set key comparison is insensitive to the difference between -0 and 0.

The only places I am aware of where the programmer needs to be aware of the difference, if they do not otherwise care, is:

  • 1/-0 === -Infinity while 1/0 === Infinity
  • Object.is(-0, 0) is false.
  • defineProperty on a non-writable non-configurable property can set 0 to 0 and -0 to -0, but it cannot change one to the other.
  • They serialize to bits differently.

Most programs that do not otherwise care about the difference between -0 and 0 will also not care about the bullet points above. This is also a hazard, as these bullet points may violate the principle of least surprise. However, the first bullet is mandated by IEEE and ancient JS, necessitating the other bullets.


Had String(-0) or JSON.stringify(-0) been different from String(0) or JSON.stringify(0), this would require programmers to care about the difference in more cases; in particular, in cases not necessitated by the initial IEEE mandated difference above.

@Pauan
Copy link

Pauan commented Mar 11, 2019

@erights There is also Math.pow and Math.atan2.

What about the situations where somebody does care about the distinction between 0 and -0? Right now that distinction is completely lost (which can lead to very subtle bugs).

I can imagine a server/client exchanging messages, and that message might be 0 or -0, and the application cares about the distinction.

So is the recommendation in that case to completely avoid JSON.stringify and use a custom serialization system? That is doable, but seems like a footgun which most people won't be aware of.

It also seems odd that JSON.parse preserves -0 but JSON.stringify does not, so the API is internally inconsistent. And that means that JSON does not round-trip, which seems undesirable.

It also has implications for a client (written in JavaScript and using JSON.stringify) which then sends a JSON message to a server (written in a non-JavaScript language which cares more about -0).

My opinion is that it's okay for JavaScript as a language to not care about -0, but the JSON data format should care, since it has implications for things outside of JavaScript. So I think JSON.stringify is a special case.

@erights
Copy link

erights commented Mar 12, 2019

There is also Math.pow and Math.atan2.

Good point, thanks!

What about the situations where somebody does care about the distinction between 0 and -0?

The decision was already made for String(-0) before my time. I am just trying to explain my sense of the rationale. Given the behavior of String(-0), I think the behavior of JSON.stringify(-0) must remain as is

So is the recommendation in that case to completely avoid JSON.stringify and use a custom serialization system?

Note that JSON also cannot directly represent NaN, Infinity, and -Infinity, so it is far from a round trip encoder of IEEE floating point values, even aside from -0. Round tripping requires an additional level of encoding anyway, for which you can use replacers and revivers. See

https://github.com/Agoric/PlaygroundVat/blob/master/src/vat/webkey.js#L181

@erights
Copy link

erights commented Mar 12, 2019

And https://github.com/Agoric/PlaygroundVat/blob/master/src/vat/webkey.js#L345

@mathiasbynens
Copy link
Member Author

Closing now that the historical context has been clarified. Thanks, everyone!

@mathiasbynens mathiasbynens deleted the json-stringify-negative-zero branch March 15, 2019 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs consensus This needs committee consensus before it can be eligible to be merged. needs data This PR needs more information; such as web compatibility data, “web reality” (what all engines do)… needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 normative change Affects behavior required to correctly evaluate some ECMAScript source text

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants