Skip to the code
cf.

JSON.parse

Write JSON.myParse(text), which reads a string of JSON and returns the value it describes, like JSON.parse(text), without calling JSON.parse, eval, or new Function. There's no reviver argument.

text holds exactly one value. Whitespace may come before it, after it, and between its parts, such as around : and ,. Only spaces, tabs, newlines, and carriage returns count as whitespace.

true, false, and null are written exactly so, in lowercase.

A number is an optional -, then either 0 or a digit from 1 to 9 followed by any number of digits, then optionally a . and at least one digit, then optionally an e or E, an optional + or -, and at least one digit. So -0.5e10 is a number, but 01, .5, 5., +1, NaN, and Infinity aren't. Its value is what Number returns for that text.

A string is written in double quotes. Any character can appear in it as it is, except ", \, and the control characters below U+0020 (the space), such as a raw newline or tab. A backslash starts an escape: \", \\, \/, \b, \f, \n, \r, and \t each stand for one character, and \u followed by exactly four hex digits stands for the UTF-16 code unit with that number. Any other escape is an error.

An array is values separated by commas, between [ and ]. An object is members separated by commas, between { and }, where a member is a string key, a :, and a value. Either can be empty, but every comma must sit between two items: no leading, trailing, or doubled commas.

Arrays come back as plain arrays, and objects as plain objects with their properties in the order the keys first appear. If a key appears twice, the last value wins. A key named "__proto__" becomes an ordinary own property, as it does with JSON.parse: it never changes the object's prototype.

For any text that breaks these rules, throw a SyntaxError. Its message must include at position N, where N is the index of the first character that doesn't fit, or the length of text if the text ends too soon.

JSON.myParse('{"name": "Ada", "tags": ["x", null], "age": 36.5}');
// { name: "Ada", tags: ["x", null], age: 36.5 }
JSON.myParse('"caf\\u00e9\\n"');
// "café\n": the escapes become é and a newline
JSON.myParse("[1, 2,]");
// SyntaxError: Unexpected character "]" at position 6
JSON.myParse('{"a": 1');
// SyntaxError: Unexpected end of JSON input at position 7

What the tests check

  1. reads true, false, null, and numbers
    expect(JSON.myParse("true")).toBe(true);
    expect(JSON.myParse("false")).toBe(false);
    expect(JSON.myParse("null")).toBeNull();
    for (const text of ["0", "-12", "3.25", "1e3", "2E-2", "-0.5e+1", "-0", "1234567890"]) {
      expect(JSON.myParse(text)).toBe(Number(text));
    }
    
  2. reads strings, with every escape
    expect(JSON.myParse('"hello"')).toBe("hello");
    expect(JSON.myParse('""')).toBe("");
    expect(JSON.myParse('"say \\"hi\\""')).toBe('say "hi"');
    expect(JSON.myParse('"back\\\\slash \\/"')).toBe("back\\slash /");
    expect(JSON.myParse('"\\b\\f\\n\\r\\t"')).toBe("\b\f\n\r\t");
    expect(JSON.myParse('"caf\\u00e9 \\u00E9"')).toBe("café é");
    expect(JSON.myParse('"\\ud83d\\ude00"')).toBe("😀");
    expect(JSON.myParse('"café ✓ <tag>"')).toBe("café ✓ <tag>");
    
  3. reads nested arrays and objects, with whitespace between the parts
    const text = ' {\n\t"name" : "Ada" ,\r\n "tags" : [ 1 , [ ] , { } , [ true , null ] ] } ';
    expect(JSON.myParse(text)).toEqual({ name: "Ada", tags: [1, [], {}, [true, null]] });
    expect(JSON.myParse("[]")).toEqual([]);
    expect(JSON.myParse("{}")).toEqual({});
    expect(JSON.myParse('[[[["deep"]]]]')).toEqual([[[["deep"]]]]);
    
  4. makes plain objects, where the last value of a repeated key wins
    const value = JSON.myParse('{"b": 1, "a": 2, "b": 3}');
    expect(Object.getPrototypeOf(value)).toBe(Object.prototype);
    expect(value).toEqual({ b: 3, a: 2 });
    expect(Object.keys(value)).toEqual(["b", "a"]);
    expect(Array.isArray(JSON.myParse("[1]"))).toBe(true);
    
  5. makes __proto__ an ordinary own key
    const value = JSON.myParse('{"__proto__": {"admin": true}, "name": "x"}');
    expect(Object.getPrototypeOf(value)).toBe(Object.prototype);
    expect(value.admin).toBeUndefined();
    expect(Object.keys(value)).toEqual(["__proto__", "name"]);
    expect(Object.getOwnPropertyDescriptor(value, "__proto__").value).toEqual({ admin: true });
    
  6. throws a SyntaxError for broken arrays and objects
    const broken = ["", "   ", "[1, 2", "[1, 2,]", "[,1]", "[1,,2]", "[1 2]", "{", '{"a": 1,}', '{"a" 1}', '{"a": }', '{"a": 1 "b": 2}', "{a: 1}", "{'a': 1}", '{"a": 1}}', "[1] [2]", "{} x"];
    // Lists every text that didn't throw a SyntaxError.
    const missed = broken.filter((text) => {
      try {
        JSON.myParse(text);
        return true;
      } catch (error) {
        return !(error instanceof SyntaxError);
      }
    });
    expect(missed).toEqual([]);
    
  7. throws a SyntaxError for numbers, words, strings, and whitespace JSON doesn't allow
    const invalid = ["01", "-", "-a", ".5", "5.", "1.e3", "1e", "+1", "0x1F", "NaN", "Infinity", "undefined", "tru", "True", "nulll", "'text'", '"open', '"tab\there"', '"line\nbreak"', '"\\x41"', '"\\u12"', '"\\u12G4"', "\u00a01", "\v1", "\f1"];
    // Lists every text that didn't throw a SyntaxError, with invisible characters shown as <U+…>.
    const visible = (text) => text.replace(/[\u0000-\u001f\u00a0]/g, (char) => "<U+" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + ">");
    const missed = invalid.filter((text) => {
      try {
        JSON.myParse(text);
        return true;
      } catch (error) {
        return !(error instanceof SyntaxError);
      }
    });
    expect(missed.map(visible)).toEqual([]);
    
  8. says where the problem is, with at position N in the message
    expect(() => JSON.myParse("[1, 2,]")).toThrow(/at position 6\b/);
    expect(() => JSON.myParse('{"a" 1}')).toThrow(/at position 5\b/);
    expect(() => JSON.myParse('{"list": [1, 2 3]}')).toThrow(/at position 15\b/);
    expect(() => JSON.myParse("{} x")).toThrow(/at position 3\b/);
    expect(() => JSON.myParse("{'a': 1}")).toThrow(/at position 1\b/);
    expect(() => JSON.myParse('{"a": 1')).toThrow(/at position 7\b/);
    expect(() => JSON.myParse("")).toThrow(/at position 0\b/);
    
  9. works without JSON.parse, eval, or new Function
    const builtIn = { parse: JSON.parse, eval: globalThis.eval, Function: globalThis.Function };
    JSON.parse = function () {
      throw new Error("myParse called the built-in JSON.parse");
    };
    globalThis.eval = function () {
      throw new Error("myParse called eval");
    };
    globalThis.Function = function () {
      throw new Error("myParse called new Function");
    };
    let result;
    try {
      result = JSON.myParse('{"a": [1, "b", null], "c": true}');
    } finally {
      JSON.parse = builtIn.parse;
      globalThis.eval = builtIn.eval;
      globalThis.Function = builtIn.Function;
    }
    expect(result).toEqual({ 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).