Skip to content
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

Normative: Make `JSON.stringify(-0)` preserve the sign #1466

Closed
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
7 participants
@mathiasbynens
Copy link
Member

mathiasbynens commented Mar 5, 2019

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
@ljharb

ljharb approved these changes Mar 5, 2019

Copy link
Member

ljharb left a comment

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 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 zenparsing and tc39/ecma262-editors Mar 5, 2019

Normative: Make JSON.stringify(-0) preserve the sign
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 Mar 5, 2019

@claudepache

This comment has been minimized.

Copy link
Contributor

claudepache commented Mar 5, 2019

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

This comment has been minimized.

Copy link
Member Author

mathiasbynens commented Mar 5, 2019

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

This comment has been minimized.

Copy link
Contributor

claudepache commented Mar 5, 2019

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member Author

mathiasbynens commented Mar 6, 2019

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

@claudepache

This comment has been minimized.

Copy link
Contributor

claudepache commented Mar 6, 2019

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

waldemarhorwat commented Mar 8, 2019

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

@ljharb

This comment has been minimized.

Copy link
Member

ljharb commented Mar 8, 2019

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

@mathiasbynens

This comment has been minimized.

Copy link
Member Author

mathiasbynens commented Mar 11, 2019

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

@mathiasbynens

This comment has been minimized.

Copy link
Member Author

mathiasbynens commented Mar 15, 2019

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

@mathiasbynens mathiasbynens deleted the json-stringify-negative-zero branch Mar 15, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.