Skip to the code
cf.

JSON.stringify

Write JSON.myStringify(value), which returns the same text as JSON.stringify(value) for the values described here, without calling JSON.stringify. There's no replacer or space argument, and no spaces in the output.

true, false, and null are written as they are. Finite numbers are written the way String(number) writes them, and NaN, Infinity, and -Infinity become null.

Strings go in double quotes. Inside them, " and \ get a backslash in front; backspace, form feed, newline, carriage return, and tab become \b, \f, \n, \r, and \t; any other character whose code is below 32 (U+0020, the space) becomes \u and four lowercase hex digits, like \u001f. Every other character is written as it is.

An array becomes its elements between [ and ], separated by commas, with holes written as null. Any other object becomes "key":value pairs between { and }, separated by commas, for its own enumerable string keys in Object.keys order. Keys are quoted and escaped like strings.

undefined, functions, and symbols have no JSON form. As an object's property they're left out, in an array they're written as null, and on their own JSON.myStringify returns undefined, not a string.

If an object has a toJSON method, call it with the object's key (the property name, the index as a string inside an array, or "" at the top level) and write what it returns instead. That's how a Date becomes its ISO string.

Throw a TypeError for an object that contains itself, directly or further down (a cycle). The same object appearing twice without a cycle is fine.

BigInts, boxed primitives like new String("a"), and lone surrogates are out of scope.

JSON.myStringify({ name: "Ada", tags: ["x", undefined], skip: undefined });
// {"name":"Ada","tags":["x",null]}
JSON.myStringify({ when: new Date(0) });
// {"when":"1970-01-01T00:00:00.000Z"}
JSON.myStringify('say "hi"\n');
// "say \"hi\"\n"
JSON.myStringify(() => {});
// undefined

What the tests check

  1. writes numbers, booleans, and null, with NaN and Infinity as null
    expect(JSON.myStringify(42)).toBe("42");
    expect(JSON.myStringify(-1.5)).toBe("-1.5");
    expect(JSON.myStringify(1e21)).toBe("1e+21");
    expect(JSON.myStringify(-0)).toBe("0");
    expect(JSON.myStringify(true)).toBe("true");
    expect(JSON.myStringify(false)).toBe("false");
    expect(JSON.myStringify(null)).toBe("null");
    expect(JSON.myStringify(NaN)).toBe("null");
    expect(JSON.myStringify(-Infinity)).toBe("null");
    
  2. quotes strings and escapes quotes, backslashes, and control characters
    expect(JSON.myStringify("hi")).toBe('"hi"');
    const samples = ['say "hi"', "back\\slash", "line\nbreak", "tab\tand\rreturn", "\b\f", "\u0001\u001f", "café ✓"];
    for (const text of samples) {
      expect(JSON.myStringify(text)).toBe(JSON.stringify(text));
    }
    
  3. writes arrays and objects with no spaces, own keys in order
    expect(JSON.myStringify([1, "a", [true, null], []])).toBe('[1,"a",[true,null],[]]');
    expect(JSON.myStringify({ b: 1, a: { c: {} }, 2: "two", 1: "one" })).toBe('{"1":"one","2":"two","b":1,"a":{"c":{}}}');
    const child = Object.create({ inherited: 1 });
    child.own = 2;
    expect(JSON.myStringify(child)).toBe('{"own":2}');
    expect(JSON.myStringify({ 'say "hi"': 1 })).toBe('{"say \\"hi\\"":1}');
    
  4. leaves undefined, functions, and symbols out of objects
    const value = { a: 1, b: undefined, c() {}, d: Symbol("d"), e: "kept" };
    expect(JSON.myStringify(value)).toBe('{"a":1,"e":"kept"}');
    expect(JSON.myStringify({ only: undefined })).toBe("{}");
    
  5. writes null for holes and for undefined, functions, and symbols in arrays
    expect(JSON.myStringify([undefined, () => {}, Symbol("s")])).toBe("[null,null,null]");
    expect(JSON.myStringify([1, , 3])).toBe("[1,null,3]");
    
  6. returns undefined for undefined, a function, or a symbol on its own
    expect(JSON.myStringify(undefined)).toBeUndefined();
    expect(JSON.myStringify(() => {})).toBeUndefined();
    expect(JSON.myStringify(Symbol("s"))).toBeUndefined();
    
  7. calls toJSON with the key and writes its result
    const date = new Date(Date.UTC(2024, 0, 2, 3, 4, 5));
    expect(JSON.myStringify({ when: date })).toBe('{"when":"2024-01-02T03:04:05.000Z"}');
    const keys = [];
    const item = {
      toJSON(key) {
        keys.push(key);
        return { id: 7 };
      },
    };
    expect(JSON.myStringify({ item, list: [item] })).toBe('{"item":{"id":7},"list":[{"id":7}]}');
    expect(JSON.myStringify(item)).toBe('{"id":7}');
    expect(keys).toEqual(["item", "0", ""]);
    
  8. throws a TypeError for a cycle, but not for a shared object
    const shared = { n: 1 };
    expect(JSON.myStringify({ a: shared, b: [shared] })).toBe('{"a":{"n":1},"b":[{"n":1}]}');
    const loop = { name: "loop" };
    loop.self = loop;
    expect(() => JSON.myStringify(loop)).toThrow(TypeError);
    const list = [];
    list.push({ list });
    expect(() => JSON.myStringify(list)).toThrow(TypeError);
    
  9. works without the built-in JSON.stringify
    const builtIn = JSON.stringify;
    JSON.stringify = function () {
      throw new Error("myStringify called the built-in JSON.stringify");
    };
    let result;
    try {
      result = JSON.myStringify({ a: [1, "b", null], c: true });
    } finally {
      JSON.stringify = builtIn;
    }
    expect(result).toBe('{"a":[1,"b",null],"c":true}');
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).