-
Notifications
You must be signed in to change notification settings - Fork 62
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
Boxes: unwrap when calling JSON.stringify? #232
Comments
We are likely to do it, ignoring it would be footgunny and the alternative is to throw. |
I'm trying to write the spec text for this, but it's really hard to decide on a good behavior.
Replying "yes" to (1) lets us respect the |
I thought Boxes were not objects, so it shouldn't be trying to call |
No, but the algorithm will either first check if it's an object with JSON.stringify(Box({ toJSON() { return { x: 1 } } }));
JSON.stringify({ toJSON() { return Box({ x: 1 }) } }); I think it's only possible to make only one of those to return |
Could it not recurse? If the value is a box type, then it is unboxed and the algo starts again on the unboxed value? Sorry if I'm missing something! Though I like the idea of the replacer being called before unboxing a box, i.e. passing the Box directly to the replacer. That way you could create a bi-directional replacer and reviver pair that does recreate the box on JSON.parse. Maybe something like this: function replacer(key, value) {
if (Box.isBox(value)) {
return { __box: true, value: value.unbox() };
}
...
}
function reviver(key, value) {
if (value?.__box) {
return Box(reviver(key, value.value));
}
...
} |
Potentially yes, and that was the first approach I tried. However, in this case it only calls JSON.stringify({
toJSON() {
return {
toJSON() {
return { x: 1 };
}
};
}
}); so this should probably call JSON.stringify({
toJSON() {
return Box({
toJSON() {
return { x: 1 };
}
});
}
}); |
Thanks for that example, I can see the issue now :) |
If we can't find a solution, we should remember that there is already another primitive that throws: bigints. Users can either define |
I really hope we don’t follow that example; it drastically hinders usability. |
So I think this is solvable, similar to how the ecma-262 gramma is defined, by using a flag. As we recurse if we store a function replacer(key, value) {return value};
JSON.stringify({ //------------ skipped (toJSON)
prop1: 1, //--------------- skipped (toJSON)
toJSON() { //-------------- called
return { //-------------- passed via replacer {
prop2: 2, //----------- passed via replacer "prop2": 2,
toJSON() { // --------- passed via replacer, skipped (function)
return { x: 1 };
},
prop3: { //------------ skipped (toJSON) "prop3":
toJSON() { //------ called
return { //---- passed via replacer {
y: 2 //---- passed via replacer "y": 2
}; // }
}
}
}; // }
}
}, replacer);
JSON.stringify({ //------------ passed via replacer {
prop1: 1, // -------------- passed via replacer "prop1": 1,
prop2: { //---------------- skipped "prop2":
prop3: 2, //----------- skipped
toJSON() { //---------- called
return { //---------- passed via replacer {
x: 1 //---------- passed via replacer "x": 1
}; // }
}
}
}, replacer); // }
With boxes: JSON.stringify(Box( //--------- passed via replacer, auto-unbox
{ //----------------------- skipped (toJSON)
toJSON() { //---------- called
return { //-------- passed via replacer {
x: 1 //-------- passed via replacer "x": 1
}; // }
}
}
), replacer);
JSON.stringify({ //------------ skipped (toJSON)
toJSON() { //-------------- called
return Box( //--------- passed via replacer, auto-unbox
{ //--------------- passed via replacer {
x: 1 // "x": 1
} // }
);
}
}, replacer);
|
That's a really interesting idea! I'm also going to search about the history of |
Good idea to check the history of it. Looking at the class Base {
toJSON() {
... // custom JSON behavior
}
}
class SubClass extends Base {
/** @override */
toJSON() {
return this; // SubClass wants to revert to standard JSON serilisation
}
} |
For reference, I found this email discussing |
As I mentioned in #254 (comment), I believe the approach taken is trying to be too smart and too accommodating of unclear behavior.
IMO this should return The way I understand This is the behavior I propose: |
It also solves the problem of the conflicting behavior with the replacer: JSON.stringify(
{
box: Box({ x: 1, toJSON: () => ({ y: 2 }) }),
},
(k, v) => v
); Would expectedly return Edit: it would also not hide intermediate |
Thinking a little more about the recurse, I think the approach could be to pass the box as
|
One of the reasons for calling the replacer before unboxing, is so the serialised string can retain the position where boxes were. That way the reviver can more closely reconstruct the original value. |
Agreed, and that's still the case with my proposed algorithm. |
Got my head around the technique now, I hadn't caught that Box goes around twice, first as a value, then as a holder. I've added the diff comments incase it helps others too. +1. If Type(_holder_) is Box, then
+ 1. Let _value_ be ? Unbox(_holder_).
+1. Else,
1. Let _value_ be ? Get(_holder_, _key_).
1. Let _toJSONCalled_ be *false*.
1. If Type(_value_) is Object or BigInt, then
1. Let _toJSON_ be ? GetV(_value_, *"toJSON"*).
1. If IsCallable(_toJSON_) is *true*, then
1. Set _value_ to ? Call(_toJSON_, _value_, « _key_ »).
1. Set _toJSONCalled_ to *true*.
1. If _state_.[[ReplacerFunction]] is not *undefined*, then
- 1. Set _value_ to ? Call(_state_.[[ReplacerFunction]], _holder_, « _key_, _value_ »).
- 1. Set _value_ to ! MaybeUnwrapBox(_value_).
-1. Else,
- 1. Set _value_ to ! MaybeUnwrapBox(_value_).
- 1. If _toJSONCalled_ is *false*, then
- 1. If Type(_value_) is Object or BigInt, then
- 1. Let _toJSON_ be ? GetV(_value_, *"toJSON"*).
- 1. If IsCallable(_toJSON_) is *true*, then
- 1. Set _value_ to ? Call(_toJSON_, _value_, « _key_ »).
- 1. Set _value_ to ! MaybeUnwrapBox(_value_).
+ 1. Set _value_ to ? Call(_state_.[[ReplacerFunction]], ToObject(_holder_), « _key_, _value_ »).
1. If Type(_value_) is Object, then
1. If _value_ has a [[NumberData]] internal slot, then
1. Set _value_ to ? ToNumber(_value_).
1. Else if _value_ has a [[StringData]] internal slot, then
1. Set _value_ to ? ToString(_value_).
1. Else if _value_ has a [[BooleanData]] internal slot, then
1. Set _value_ to _value_.[[BooleanData]].
1. Else if _value_ has a [[BigIntData]] internal slot, then
1. Set _value_ to _value_.[[BigIntData]].
+ 1. Else if _value_ has a [[BoxData]] internal slot, then
+ 1. Set _value_ to _value_.[[BoxData]].
1. If _value_ is *null*, return *"null"*.
1. If _value_ is *true*, return *"true"*.
1. If _value_ is *false*, return *"false"*.
1. If Type(_value_) is Record or Type(_value_) is Tuple, set _value_ to ! ToObject(_value_).
+1. If Type(_value_) is Box,
+ 1. If _toJSONCalled_ is *false*, return ? SerializeJSONProperty(_state_, the empty String, _value_).
+ 1. Else, return *undefined*. // or throw a *TypeError* exception.
1. If Type(_value_) is String, return QuoteJSONString(_value_).
1. If Type(_value_) is Number, then
1. If _value_ is finite, return ! ToString(_value_).
1. Return *"null"*.
1. If Type(_value_) is BigInt, throw a *TypeError* exception.
1. If Type(_value_) is Object and IsCallable(_value_) is *false*, then
1. If ! IsTuple(_value_) is *true*, then
1. Let _isArrayLike_ be *true*.
1. Else,
1. Let _isArrayLike_ be ? IsArray(_value_).
1. If _isArrayLike_ is *true*, return ? SerializeJSONArray(_state_, _value_).
1. Return ? SerializeJSONObject(_state_, _value_).
1. Return *undefined*. |
👍 TIL github supports manual diffs code blocks! |
Q: are these outputs correct, according to your algorithm?
Additionally:
Also, would this be a valid "polyfill" for const box = Box({ x: 1 });
unbox(box);
function unbox(box) {
let result;
let i = 0;
JSON.stringify(box, function (key, value) {
switch (i++) {
case 0:
// It receives "" as the key and Box({ x: 1 }). Return Box({ x: 1 }).
// The algorithm then calls SerializeJSONProperty(state, "", Box({ x: 1 }))
return value;
case 1:
// 'this' is Box({ x: 1 }), and 'value' is its contents
result = value;
return {};
});
return result;
} |
I'll assume your example meant to not call
Since And then proceed as for the table in the second section with manual unboxing. Aka, serializer manually unboxing or not has no impact on how the JSON serialization would happen for boxes. The only impacted objects by the let obj = {
toJSON() : function() {
return Box({ x: 1 });
},
};
Fair, I'll think about how to integrate it.
Edit: my virtualization is wrong, please ignore. It wouldn't since the algorithm calls
function toJSON(key) {
const target = unwrap(this);
const targetToJSON = target.toJSON;
if (typeof targetToJSON === 'function') {
return targetToJSON.call(target, key);
}
return target;
}
function Box(target) {
// ...
wrappedTarget = Object.freeze({
target,
toString,
toJSON,
});
// ...
return OriginalBox(wrappedTarget);
} So the serializer only has access to the original box as |
Yes thanks, I updated the example.
That |
I believe this is the case for
It wouldn't break any virtualization because the box value itself is not virtualized (only possibly the content). |
I wasn't thinking clearly on Friday apparently, my virtualization doesn't work. Apologies. I do however believe my proposed change to the JSON.stringify algorithm has less gotchas than the current one. |
Np, I created a playground with simple |
Neat!
I believe this is the same as "current proposal", or did I miss something? |
No: with the current proposal "current proposal" = "what we have have at https://tc39.es/proposal-record-tuple/#sec-json.stringify" |
Ugh, I apparently opened the wrong link. |
Nit: the Box.prototype.toJSON = function () {
let cur = this;
while (Type(cur) === 'box') cur = Box.unbox(cur);
return cur;
} |
Thanks, I updated the playground link |
Actually, worse, it's need to call Box.prototype.toJSON = function (key) {
const content = Box.unbox(this);
if ('toJSON' in content) {
return content.toJSON(key);
}
return content;
} Anyway, this shows requiring users to manually implement a transparent |
A fourth option would be to still rely on |
Yeah, but as I wrote in #254 (comment), that would make locked down compartments surprising in that they wouldn't behave the same. |
We removed boxes from the proposal, since it was stalled because of them. I'm closing this issue, but we'll keep track of it if we'll bring them up again as a follow on proposal. |
As part of #197 it was proposed to unwrap boxes when JSON.stringifying.
Do we want to make that change? if yes, this ticket will track the change...
The text was updated successfully, but these errors were encountered: