Set `Object.prototype.toJSON` a non-configurable, non-writable `null`

The current spec allows to define Object.prototype.toJSON as desired, which can affect JSON.stringify.

Object.prototype.toJSON = () => "foo";
JSON.stringify({ bar: "baz" }); // '"foo"'

To avoid this, I propose to define a non-configurable, non-writeable property of null for Object.prototype.toJSON. This is similar to the discussion of Function.prototype[Symbol.metadata] for Decorator Metadata proposal.

There’s a constraint from implementers that it be syntactically determinable whether a class should be given a Symbol.metadata property. This means it can be conditional based on the presence of decorators, but not by the actual usage of metadata by decorators. Additionally, we all agree that we don’t want it to be possible to add metadata to everything by modifying Function.prototype. The only way it turns out to be practical to do that is to add a non-configurable, non-writeable property of null to Function.prototype, to avoid injecting metadata on everyone. Given those constraints, there’s only one design that makes sense, and that’s the design everyone likes.

Such a change would trigger the "override mistake" for existing code which uses an assignment to add a toJSON property to their custom objects (or prototype objects), and as such is a non starter without a mitigation in place for the override mistake.

1 Like

Oh, I see. It seems that simple changes are problematic.

Object.defineProperty(Object.prototype, "toJSON", {
  value: null,
  writable: false,
  enumerable: false,
  configurable: false,
});

const obj = {};
obj.toJSON = () => "foo"; // throws TypeError

@mhofman Could this be solved by making it an accessor property?

// instead of internal slot
const wm = new WeakMap();
wm.set(Object.prototype, null);

Object.defineProperty(Object.prototype, "toJSON", {
  get() {
    return wm.get(this);
  },
  set(val) {
    if (this === Object.prototype) {
      throw new TypeError();
    }
    wm.set(this, val);
  },
  configurable: false,
  enumerable: false,
});

const obj = {};
obj.toJSON = () => "foo"; // valid

You’d make it a setter that just did a defineOwnProperty. but, that’d still make 'toJSON' in x return true for a bunch of places where it previously returned false.

2 Likes

We could, in principle, make Object.prototype be more exotic, and reject some or all new properties being added to it without triggering the override mistake. It's already exotic in that it prevents you from changing its [[Prototype]].

I've suggested that elsewhere. I'm not sure it will be web-compatible, but it would fix a lot of issues. I think it's more likely we'll be able to get away with a more limited change, like making it reject Symbol-named properties and possible numeric properties but still accept string-named properties, but maybe we could also include toJSON in that list.

3 Likes