Implementing Custom `JSON.parse` in JavaScript



Implementing a custom JSON.parse function in JavaScript is a complex task that involves understanding and handling JSON syntax, types, and structures. In this episode, we'll create a simplified version of JSON.parse to convert a JSON string into a JavaScript object.


What is JSON.parse?

JSON.parse is a method that parses a JSON string, constructing the JavaScript value or object described by the string. It's widely used to deserialize JSON data received from a network or stored in a text-based format.

Real Interview Insights

Interviewers might ask you to:

  • Implement a custom JSON.parse function.
  • Handle various data types, including objects, arrays, strings, numbers, booleans, and null.
  • Address edge cases and errors, such as malformed JSON strings.

Implementing Custom JSON.parse

Here’s a basic implementation of a custom JSON.parse function:

function customParse(json) {
  let index = 0;
  const str = json;
 
  function parseValue() {
    skipWhitespace();
    const char = str[index];
 
    if (char === '"') return parseString();
    if (char === '{') return parseObject();
    if (char === '[') return parseArray();
    if (char === 't') return parseLiteral('true', true);
    if (char === 'f') return parseLiteral('false', false);
    if (char === 'n') return parseLiteral('null', null);
    if (isDigit(char) || char === '-') return parseNumber();
 
    throw new SyntaxError(`Unexpected character: ${char}`);
  }
 
  function parseString() {
    let result = '';
    index++; // Skip initial quote
 
    while (index < str.length) {
      const char = str[index++];
      if (char === '"') break;
      result += char;
    }
 
    return result;
  }
 
  function parseObject() {
    const result = {};
    index++; // Skip initial brace
    skipWhitespace();
 
    if (str[index] === '}') {
      index++; // Skip closing brace
      return result;
    }
 
    while (index < str.length) {
      const key = parseString();
      skipWhitespace();
      index++; // Skip colon
      skipWhitespace();
      const value = parseValue();
      result[key] = value;
      skipWhitespace();
 
      if (str[index] === '}') {
        index++; // Skip closing brace
        return result;
      }
 
      index++; // Skip comma
      skipWhitespace();
    }
 
    throw new SyntaxError('Unexpected end of input');
  }
 
  function parseArray() {
    const result = [];
    index++; // Skip initial bracket
    skipWhitespace();
 
    if (str[index] === ']') {
      index++; // Skip closing bracket
      return result;
    }
 
    while (index < str.length) {
      result.push(parseValue());
      skipWhitespace();
 
      if (str[index] === ']') {
        index++; // Skip closing bracket
        return result;
      }
 
      index++; // Skip comma
      skipWhitespace();
    }
 
    throw new SyntaxError('Unexpected end of input');
  }
 
  function parseLiteral(literal, value) {
    for (let i = 0; i < literal.length; i++) {
      if (str[index++] !== literal[i]) {
        throw new SyntaxError(`Unexpected token: ${literal}`);
      }
    }
    return value;
  }
 
  function parseNumber() {
    let start = index;
    if (str[index] === '-') index++; // Skip minus
    while (isDigit(str[index])) index++; // Skip digits
    if (str[index] === '.') {
      index++; // Skip dot
      while (isDigit(str[index])) index++; // Skip digits
    }
    if (str[index] === 'e' || str[index] === 'E') {
      index++; // Skip e
      if (str[index] === '+' || str[index] === '-') index++; // Skip sign
      while (isDigit(str[index])) index++; // Skip digits
    }
 
    return Number(str.slice(start, index));
  }
 
  function isDigit(char) {
    return char >= '0' && char <= '9';
  }
 
  function skipWhitespace() {
    while (str[index] === ' ' || str[index] === '\n' || str[index] === '\t' || str[index] === '\r') {
      index++;
    }
  }
 
  return parseValue();
}
Explanation:
  • Whitespace Handling: Skip whitespace characters to correctly parse values.
  • String Parsing: Handle double-quoted strings.
  • Object Parsing: Handle key-value pairs enclosed in curly braces.
  • Array Parsing: Handle values enclosed in square brackets.
  • Literal Parsing: Handle true, false, and null.
  • Number Parsing: Handle numeric values, including negative numbers, decimals, and scientific notation.

Practical Example

Consider an example with various data types:

const jsonString = `
{
  "name": "John",
  "age": 30,
  "isStudent": false,
  "hobbies": ["reading", "gaming"],
  "address": {
    "city": "New York",
    "zip": 10001
  },
  "nullValue": null,
  "nestedArray": [[1, 2], [3, 4]],
  "numberWithExponents": 1.23e+3
}
`;
 
const parsedObject = customParse(jsonString);
console.log(parsedObject);
// Output: 
// {
//   name: 'John',
//   age: 30,
//   isStudent: false,
//   hobbies: [ 'reading', 'gaming' ],
//   address: { city: 'New York', zip: 10001 },
//   nullValue: null,
//   nestedArray: [ [ 1, 2 ], [ 3, 4 ] ],
//   numberWithExponents: 1230
// }

Handling Edge Cases

  1. Malformed JSON: Detect and throw errors for invalid JSON syntax.
  2. Complex Nesting: Correctly parse deeply nested structures.
  3. Special Characters in Strings: Handle escape sequences and special characters in strings.

Enhanced Implementation with Error Handling

function customParse(json) {
  let index = 0;
  const str = json;
 
  function parseValue() {
    skipWhitespace();
    const char = str[index];
 
    if (char === '"') return parseString();
    if (char === '{') return parseObject();
    if (char === '[') return parseArray();
    if (char === 't') return parseLiteral('true', true);
    if (char === 'f') return parseLiteral('false', false);
    if (char === 'n') return parseLiteral('null', null);
    if (isDigit(char) || char === '-') return parseNumber();
 
    throw new SyntaxError(`Unexpected character: ${char}`);
  }
 
  function parseString() {
    let result = '';
    index++; // Skip initial quote
 
    while (index < str.length) {
      const char = str[index++];
      if (char === '"') break;
      if (char === '\\') {
        const escapeChar = str[index++];
        switch (escapeChar) {
          case '"': result += '"'; break;
          case '\\': result += '\\'; break;
          case '/': result += '/'; break;
          case 'b': result += '\b'; break;
          case 'f': result += '\f'; break;
          case 'n': result += '\n'; break;
          case 'r': result += '\r'; break;
          case 't': result += '\t'; break;
          default: throw new SyntaxError(`Invalid escape sequence: \\${escapeChar}`);
        }
      } else {
        result += char;
      }
    }
 
    return result;
  }
 
  function parseObject() {
    const result = {};
    index++; // Skip initial brace
    skipWhitespace();
 
    if (str[index] === '}') {
      index++; // Skip closing brace
      return result;
    }
 
    while (index < str.length) {
      const key = parseString();
      skipWhitespace();
      if (str[index++] !== ':') throw new SyntaxError('Expected colon after key in object');
      skipWhitespace();
      const value = parseValue();
      result[key] = value;
      skipWhitespace();
 
      if (str[index] === '}') {
        index++; // Skip closing brace
        return result;
      }
 
      if (str[index++] !== ',') throw new SyntaxError('Expected comma after pair in object');
      skipWhitespace();
    }
 
    throw new SyntaxError('Unexpected end of input');
  }
 
  function parseArray() {
    const result = [];
    index++; // Skip initial bracket
    skipWhitespace();
 
    if (str[index] === ']') {
      index++; // Skip closing bracket
      return result;
    }
 
    while (index < str.length) {
      result.push(parseValue());
      skipWhitespace();
 
      if (str[index] === ']') {
        index++; // Skip closing bracket
        return result;
      }
 
      if (str[index++] !== ',') throw new SyntaxError('Expected comma after value in array');
      skipWhitespace();
    }
 
    throw new SyntaxError('Unexpected end of input');
  }
 
  function parseLiteral(literal, value) {
    for (let i = 0; i < literal.length; i++) {
      if (str[index++] !== literal[i]) {
        throw new SyntaxError(`Unexpected token: ${literal}`);
      }
    }
    return value
 
;
  }
 
  function parseNumber() {
    let start = index;
    if (str[index] === '-') index++; // Skip minus
    while (isDigit(str[index])) index++; // Skip digits
    if (str[index] === '.') {
      index++; // Skip dot
      while (isDigit(str[index])) index++; // Skip digits
    }
    if (str[index] === 'e' || str[index] === 'E') {
      index++; // Skip e
      if (str[index] === '+' || str[index] === '-') index++; // Skip sign
      while (isDigit(str[index])) index++; // Skip digits
    }
 
    return Number(str.slice(start, index));
  }
 
  function isDigit(char) {
    return char >= '0' && char <= '9';
  }
 
  function skipWhitespace() {
    while (str[index] === ' ' || str[index] === '\n' || str[index] === '\t' || str[index] === '\r') {
      index++;
    }
  }
 
  return parseValue();
}
 
// Example usage with escape sequences and edge cases
const jsonString = `
{
  "name": "John \\"Doe\\"",
  "escaped": "\\\\",
  "newline": "Line1\\nLine2",
  "tab": "Column1\\tColumn2"
}`;
 
const parsedObject = customParse(jsonString);
console.log(parsedObject);
// Output: 
// {
//   name: 'John "Doe"',
//   escaped: '\\',
//   newline: 'Line1\nLine2',
//   tab: 'Column1\tColumn2'
// }

Use Cases for Custom JSON.parse

  1. Custom Deserialization: Tailoring the parsing process to fit specific needs.
  2. Debugging: Providing better control over how JSON strings are converted to objects for debugging purposes.
  3. Performance Optimization: Optimizing the parsing process for specific scenarios.