JSON-serializable object

# Marcos Caceres (6 months ago)

The Web Payment spec has the following concept defined [1]:

The term JSON-serializable object used in this specification means an object that can be serialized to a string using JSON.stringify and later deserialized back to an object using JSON.parse with no loss of data.

See [2] for example in prose.

However, it doesn't define how a UA actually would check the above... apart from basically calling:

try {
   JSON.parse(JSON.stringify(obj));
} catch(err){
   throw new Error("Not a JSON-serializable object");
}

The concept of a "JSON-serializable object" seems somewhat related to structured clones/transferables (kinda, sorta... though of course clones support some more sophisticated types/interfaces). I'm wondering if public-script-coord can help (either through WebIDL or TC39) with creating something more concrete that implementations can rely on. I'm pretty sure this has been discussed before numerous times, but I can't recall what conclusion we reached and if this is already a solved problem.

Kind regards, Marcos

[1] w3c.github.io/browser-payment-api/#dfn-json-serializable-object [2] For example, "Set the details attribute value of response to a JSON-serializable object containing the payment method specific message that will be used by the merchant to process the transaction. The format of this response will be defined for each payment method." You can see numerous instances of JSON-serializable object throughout the spec.

Contact us to advertise here
# Anne van Kesteren (6 months ago)

On Wed, Nov 16, 2016 at 7:17 AM, Marcos Caceres marcos@marcosc.com wrote:

However, it doesn't define how a UA actually would check the above...

Yeah, that seems like the main problem. No defined processing model. Is there an open issue? IDL could define something here in theory, but I don't think we've had this particular set of constraints before, whatever the processing model may be. There's a couple APIs that return JSON, but I don't know any that accept it as input (whatever that means). So I'd think that the processing model should be sorted first and then maybe it can be uplifted to IDL if it becomes a common pattern.

# Domenic Denicola (6 months ago)

From: Anne van Kesteren [mailto:annevk@annevk.nl]

On Wed, Nov 16, 2016 at 7:17 AM, Marcos Caceres marcos@marcosc.com wrote:

However, it doesn't define how a UA actually would check the above...

Yeah, that seems like the main problem. No defined processing model. Is there an open issue? IDL could define something here in theory, but I don't think we've had this particular set of constraints before, whatever the processing model may be. There's a couple APIs that return JSON, but I don't know any that accept it as input (whatever that means). So I'd think that the processing model should be sorted first and then maybe it can be uplifted to IDL if it becomes a common pattern.

I agree it is premature to solve this in IDL.

I don't think this has much to do with structured clone. My understanding is the payments spec mostly cares about being able to send something over the wire in a JSON serialized format, which is a much tighter restriction than structured clone.

I think this is simply defined as Marcos put in his original message. A JSON-serializable object is an object on which running JSON.stringify does not throw an error. As such, ideally all algorithms in the spec should be recast to something like:

  1. Let string be the result of JSON-stringifying idlObj, as if by the built-in JSON.stringify function passing idlObj converted to an ECMAScript value. If doing so would throw an exception, then, let string be null.
  2. If string is null, then
    1. ... handle the not-JSON-serializable case...
  3. Otherwise,
    1. ... handle the JSON-serializable case...
    2. (Don't forget to use the Encoding Standard to convert string to bytes.)

I think that the only real way that we could improve the situation is to define an abstract operation in ES, e.g. JSONStringify(obj), which takes care of the "as if by..." clause. That's more of a job for ES than it is for IDL.

# Anne van Kesteren (6 months ago)

On Wed, Nov 16, 2016 at 3:42 PM, Domenic Denicola d@domenic.me wrote:

A JSON-serializable object is an object on which running JSON.stringify does not throw an error.

That is not quite the same as "an object that can be serialized to a string using JSON.stringify and later deserialized back to an object using JSON.parse with no loss of data" though. E.g., you can JSON.stringify() an object with a prototype, but the object you get back after these operations does not have that prototype. So there is a data loss, but it's probably fine?

As such, ideally all algorithms in the spec should be recast to something like:

Right, that would actually make things clear.

I think that the only real way that we could improve the situation is to define an abstract operation in ES, e.g. JSONStringify(obj), which takes care of the "as if by..." clause. That's more of a job for ES than it is for IDL.

Yeah, having JSONParse(string) would help some other specifications (XMLHttpRequest and Fetch come to mind).

# Boris Zbarsky (6 months ago)

On 11/16/16 1:17 AM, Marcos Caceres wrote:

However, it doesn't define how a UA actually would check the above... apart from basically calling:

```JS try { JSON.parse(JSON.stringify(obj)); } catch(err){ throw new Error("Not a JSON-serializable object"); }

Not good enough. You can easily create a proxy which will throw on every even-numbered call to JSON.stringify, say.

A UA could check something along the lines of (for non-array objects):

1) The [[GetPrototypeOf]] of the object is tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getprototypeof

2) The prototype of the object is Object.prototype.

3) The object's [[GetOwnProperty]] is tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getownproperty-p

4) The object's [[OwnPropertyKeys]] is tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys

4) Neither the object nor Object.prototype have an accessor property named "toJSON", and if they have a value propety with that name then its value is not callable.

5) The object is not a Number object or a String object or a Boolean object.

6) The object is not callable.

7) For each string-named own property of the object, the following hold:

a) It is a value property. b) The value is an object, null, a boolean, a string, or a number. c) If the value is an object, it is itself JSON-serializable. d) If the value is a number, it's finite and is not -0.

Plus some conditions around arrays that I have not dug into. At the very least requires no holes and that all values satisfy item 7 above.

I make no claim that the above checks are sufficient. For example, I strongly suspect that if you take a Node with no extra properties defined on it and Object.setPrototypeOf its prototype to Object.prototype then it would pass all the above tests, but presumably not be considered "JSON-serializable" by the dataloss condition cited. But the above checks are certainly necessary.

Note also that whether an object is "JSON-serializable" by this definition depends on some seriously non-local state...

# Boris Zbarsky (6 months ago)

On 11/16/16 10:04 AM, Boris Zbarsky wrote:

d) If the value is a number, it's finite and is not -0.

I should point out that this "not -0" condition makes it very likely that "no dataloss" is not in fact the criterion people are after here, because I expect most scripts have no idea whether they have a -0 or not and dont care....

# Allen Wirfs-Brock (6 months ago)

This whole “JSON-serializable object” concept seems rather bogus and unhelpful. There are many reasons why a client might associate JSON.striingify "lossy data” (non-enumerable properties, built-in or object specific private state, own methods with local objects, non-indexable properties on an Array, etc) with objects for local processing purposes while still providing good&complete JSON data for the wire.

Also, because of all the dynamic hooks (toJSON methods, accessor properties, Proxies, etc) there is no way to pre-verify that JSON.stringify won’t throw other than by actually running JSON.stringify and running it multiple times may not yield the same result string. So, you should giveup on trying to “preflight” any such API arguments and deal with the reality that the ultimate application of JSON.stringify might throw.

In the payment request spec. most (perhaps all) of the uses of “JSON-serializable” data is to transmit JSON encoded data whose actual schema is defined elsewhere. For example, to encode “a payment method specific message used by the merchant to process the transaction and determine successful fund transfer”. The payment method will have to define the expected message in terms of one or more specific JSON schema (specific object properties, etc.) and the payment processor is going to have to validate that the message conforms to the schema. A “JSON-serializable object” that does not serialize to one at the expected schema is going to cause the processor to reject the transaction. So, what value is there for the client side API to validate for “JSON-serializable object” when it can’t validate the actual schema.

So, what should a spec say when its API needs to accommodate open-end JSON-encodable objects? I suggest tagging such objects as [Stringified] which is defined something like: [Stringified] This object is subject to serialization using JSON.stringify and the resulting JSON text is used for subsequent processing. It is the caller’s responsibility to pass an object that will JSON.stringify to a JSON text that is valid for the subsequent processing steps.

# Shane McCarron (6 months ago)

Are there instances where it just makes sense to pass the whole thing as a DOMString? Inside that is JSON, but who cares? It is just being passed THROUGH the browser to somewhere else. The Verifiable Claims Data Model work falls into the category, for example. So does (potentially) the payment method specific data that is in channeled through the browser payments API. It should NEVER be inspected by the browser payments API implementation. The contents are opaque. Might as well just be a string.

On Wed, Nov 16, 2016 at 12:20 PM, Allen Wirfs-Brock allen@wirfs-brock.com

wrote:

# Allen Wirfs-Brock (6 months ago)

On Nov 16, 2016, at 2:22 PM, Shane McCarron shane@spec-ops.io wrote:

Are there instances where it just makes sense to pass the whole thing as a DOMString? Inside that is JSON, but who cares? It is just being passed THROUGH the browser to somewhere else. The Verifiable Claims Data Model work falls into the category, for example. So does (potentially) the payment method specific data that is in channeled through the browser payments API. It should NEVER be inspected by the browser payments API implementation. The contents are opaque. Might as well just be a string.

That’s a real interesting approach to consider as it eliminates most of the serialization concerns from the API by making it the responsibility of the caller to do the serialization. It should be easy enough for a caller to wrap a JSON.stringify() around the object it would otherwise pass. It actually make the API more flexible because it allow other approaches other than using JSON.stringify to create the string. For example, there may well be cases where it is easier to use a template string to construct a string containing a specific JSON text then it is to construct an object graph that will serialize to that same JSON text. Also, it would permit a client to store or cache pre-serialized JSON texts without having to deserialize on each use.

Want more features?

Request early access to our private beta of readable email premium.