From Zero to AI

Lesson 5.1: JSON Parsing

Duration: 45 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Understand what JSON is and why it matters
  2. Parse JSON strings into JavaScript objects safely
  3. Convert JavaScript objects to JSON strings
  4. Handle JSON parsing errors gracefully
  5. Work with special cases like dates and undefined values

Introduction

JSON (JavaScript Object Notation) is the universal language of data exchange on the web. When you fetch data from an API, receive a webhook, or read a configuration file, you are almost certainly working with JSON. Understanding how to parse and generate JSON safely is essential for every developer.

// Raw JSON string from an API
const jsonString = '{"name": "Alice", "age": 30}';

// Parsed into a JavaScript object
const user = JSON.parse(jsonString);
console.log(user.name); // "Alice"

What is JSON?

JSON is a text format for representing structured data. It looks like JavaScript object literals but is actually a string.

JSON vs JavaScript Objects

// JavaScript object (in memory)
const jsObject = {
  name: 'Alice',
  isActive: true,
  scores: [95, 87, 92],
};

// JSON string (text representation)
const jsonString = '{"name":"Alice","isActive":true,"scores":[95,87,92]}';

JSON Rules

  1. Keys must be double-quoted strings

    { "name": "Alice" }   // Valid JSON
    { name: "Alice" }     // Invalid JSON (unquoted key)
    { 'name': 'Alice' }   // Invalid JSON (single quotes)
    
  2. Values can be:

    • Strings (double quotes only): "hello"
    • Numbers: 42, 3.14, -10
    • Booleans: true, false
    • Null: null
    • Arrays: [1, 2, 3]
    • Objects: {"key": "value"}
  3. Values cannot be:

    • Functions
    • undefined
    • Date objects (converted to strings)
    • NaN or Infinity
    • Circular references

Parsing JSON: JSON.parse()

Basic Usage

const jsonString = '{"id": 1, "name": "Alice", "email": "alice@example.com"}';

const user = JSON.parse(jsonString);

console.log(user.id); // 1
console.log(user.name); // "Alice"
console.log(user.email); // "alice@example.com"

Parsing Arrays

const jsonArray = '[1, 2, 3, 4, 5]';
const numbers: number[] = JSON.parse(jsonArray);

console.log(numbers[0]); // 1
console.log(numbers.length); // 5

Parsing Nested Objects

const complexJson = `{
  "user": {
    "name": "Alice",
    "address": {
      "city": "London",
      "country": "UK"
    }
  },
  "orders": [
    {"id": 1, "total": 99.99},
    {"id": 2, "total": 149.50}
  ]
}`;

const data = JSON.parse(complexJson);

console.log(data.user.address.city); // "London"
console.log(data.orders[0].total); // 99.99

Type Safety with JSON.parse()

The Problem

JSON.parse() returns any, which defeats TypeScript's type checking:

const data = JSON.parse('{"name": "Alice"}');
// data is type 'any' - no type safety!

console.log(data.nonExistent.property); // No error at compile time, crashes at runtime

Solution 1: Type Assertion

interface User {
  id: number;
  name: string;
  email: string;
}

const jsonString = '{"id": 1, "name": "Alice", "email": "alice@example.com"}';

const user = JSON.parse(jsonString) as User;

// Now TypeScript knows the shape
console.log(user.name); // Type-safe access

Warning: Type assertions trust the programmer. If the JSON does not match the interface, you will have runtime errors.

Solution 2: Unknown Type with Validation

function parseUser(json: string): User {
  const data: unknown = JSON.parse(json);

  // Validate the data manually
  if (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data &&
    typeof (data as User).id === 'number' &&
    typeof (data as User).name === 'string' &&
    typeof (data as User).email === 'string'
  ) {
    return data as User;
  }

  throw new Error('Invalid user data');
}

This is verbose. In Lesson 5.3, we will learn Zod, which makes this much cleaner.


Error Handling

JSON.parse() Can Throw

// Invalid JSON throws SyntaxError
try {
  JSON.parse('not valid json');
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error('Invalid JSON:', error.message);
  }
}

Common JSON Errors

// Missing quotes around key
JSON.parse('{name: "Alice"}'); // SyntaxError

// Trailing comma
JSON.parse('{"name": "Alice",}'); // SyntaxError

// Single quotes
JSON.parse("{'name': 'Alice'}"); // SyntaxError

// Undefined value
JSON.parse('{"name": undefined}'); // SyntaxError

Safe Parsing Function

function safeJsonParse<T>(
  json: string
): { success: true; data: T } | { success: false; error: Error } {
  try {
    const data = JSON.parse(json) as T;
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error('Unknown parsing error'),
    };
  }
}

// Usage
const result = safeJsonParse<User>('{"id": 1, "name": "Alice"}');

if (result.success) {
  console.log(result.data.name); // Type-safe access
} else {
  console.error('Parsing failed:', result.error.message);
}

Converting to JSON: JSON.stringify()

Basic Usage

const user = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
};

const jsonString = JSON.stringify(user);
console.log(jsonString);
// '{"id":1,"name":"Alice","email":"alice@example.com"}'

Pretty Printing

const user = { id: 1, name: 'Alice', roles: ['admin', 'user'] };

// Compact (default)
console.log(JSON.stringify(user));
// '{"id":1,"name":"Alice","roles":["admin","user"]}'

// Pretty printed with 2 spaces
console.log(JSON.stringify(user, null, 2));
/*
{
  "id": 1,
  "name": "Alice",
  "roles": [
    "admin",
    "user"
  ]
}
*/

// With tabs
console.log(JSON.stringify(user, null, '\t'));

Filtering Properties with Replacer

const user = {
  id: 1,
  name: 'Alice',
  password: 'secret123',
  email: 'alice@example.com',
};

// Include only specific keys
const publicJson = JSON.stringify(user, ['id', 'name', 'email']);
console.log(publicJson);
// '{"id":1,"name":"Alice","email":"alice@example.com"}'

// Using a function replacer
const safeJson = JSON.stringify(user, (key, value) => {
  if (key === 'password') {
    return undefined; // Exclude this property
  }
  return value;
});
console.log(safeJson);
// '{"id":1,"name":"Alice","email":"alice@example.com"}'

Special Cases

Dates

Dates are converted to ISO strings, not Date objects:

const event = {
  name: 'Meeting',
  date: new Date('2024-03-15T10:00:00Z'),
};

const json = JSON.stringify(event);
console.log(json);
// '{"name":"Meeting","date":"2024-03-15T10:00:00.000Z"}'

const parsed = JSON.parse(json);
console.log(parsed.date); // "2024-03-15T10:00:00.000Z" (string!)
console.log(typeof parsed.date); // "string"

Restoring Dates with Reviver

const json = '{"name":"Meeting","date":"2024-03-15T10:00:00.000Z"}';

const event = JSON.parse(json, (key, value) => {
  // Check if value looks like an ISO date string
  if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
});

console.log(event.date instanceof Date); // true
console.log(event.date.getFullYear()); // 2024

Undefined and Functions

These are silently removed:

const obj = {
  name: 'Alice',
  nickname: undefined,
  greet: () => 'Hello',
};

const json = JSON.stringify(obj);
console.log(json);
// '{"name":"Alice"}' - undefined and function are gone!

NaN and Infinity

These become null:

const numbers = {
  valid: 42,
  notANumber: NaN,
  infinite: Infinity,
};

const json = JSON.stringify(numbers);
console.log(json);
// '{"valid":42,"notANumber":null,"infinite":null}'

Circular References

These throw an error:

const obj: Record<string, unknown> = { name: 'Alice' };
obj.self = obj; // Circular reference

try {
  JSON.stringify(obj);
} catch (error) {
  console.error('Circular reference detected');
  // TypeError: Converting circular structure to JSON
}

Custom Serialization with toJSON()

Objects can define how they are serialized:

class User {
  constructor(
    public id: number,
    public name: string,
    private password: string
  ) {}

  toJSON() {
    // Only expose safe properties
    return {
      id: this.id,
      name: this.name,
    };
  }
}

const user = new User(1, 'Alice', 'secret123');
const json = JSON.stringify(user);
console.log(json);
// '{"id":1,"name":"Alice"}' - password not included

Practical Patterns

Type-Safe API Response Parser

interface ApiResponse<T> {
  status: 'success' | 'error';
  data?: T;
  error?: string;
}

async function fetchAndParse<T>(url: string): Promise<T> {
  const response = await fetch(url);
  const text = await response.text();

  let parsed: unknown;
  try {
    parsed = JSON.parse(text);
  } catch {
    throw new Error(`Invalid JSON response from ${url}`);
  }

  // Assuming API wraps data in a response object
  const apiResponse = parsed as ApiResponse<T>;

  if (apiResponse.status === 'error') {
    throw new Error(apiResponse.error || 'Unknown API error');
  }

  if (!apiResponse.data) {
    throw new Error('No data in response');
  }

  return apiResponse.data;
}

Configuration File Parser

interface AppConfig {
  port: number;
  database: {
    host: string;
    port: number;
    name: string;
  };
  features: string[];
}

function parseConfig(jsonString: string): AppConfig {
  let config: unknown;

  try {
    config = JSON.parse(jsonString);
  } catch (error) {
    throw new Error('Config file is not valid JSON');
  }

  // Basic validation
  if (typeof config !== 'object' || config === null) {
    throw new Error('Config must be an object');
  }

  const c = config as Record<string, unknown>;

  if (typeof c.port !== 'number') {
    throw new Error('Config.port must be a number');
  }

  if (typeof c.database !== 'object' || c.database === null) {
    throw new Error('Config.database must be an object');
  }

  if (!Array.isArray(c.features)) {
    throw new Error('Config.features must be an array');
  }

  return config as AppConfig;
}

Deep Clone with JSON

A simple (but limited) way to deep clone objects:

function deepClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

const original = {
  name: 'Alice',
  nested: { value: 42 },
};

const clone = deepClone(original);
clone.nested.value = 100;

console.log(original.nested.value); // 42 (unchanged)
console.log(clone.nested.value); // 100

// Warning: Does not work with dates, functions, undefined, etc.

Exercises

Exercise 1: Parse and Access

Parse this JSON and access nested values:

const json = `{
  "company": "Tech Corp",
  "employees": [
    {"name": "Alice", "department": "Engineering"},
    {"name": "Bob", "department": "Marketing"}
  ],
  "founded": 2015
}`;

// 1. Get the company name
// 2. Get the first employee's department
// 3. Get the number of employees
Solution
interface Employee {
  name: string;
  department: string;
}

interface Company {
  company: string;
  employees: Employee[];
  founded: number;
}

const json = `{
  "company": "Tech Corp",
  "employees": [
    {"name": "Alice", "department": "Engineering"},
    {"name": "Bob", "department": "Marketing"}
  ],
  "founded": 2015
}`;

const data = JSON.parse(json) as Company;

// 1. Company name
console.log(data.company); // "Tech Corp"

// 2. First employee's department
console.log(data.employees[0].department); // "Engineering"

// 3. Number of employees
console.log(data.employees.length); // 2

Exercise 2: Safe Stringify

Create a function that safely stringifies any value, handling circular references:

function safeStringify(value: unknown): string {
  // Return JSON string or "[Circular]" marker for circular refs
}
Solution
function safeStringify(value: unknown, space?: number): string {
  const seen = new WeakSet();

  return JSON.stringify(
    value,
    (key, val) => {
      if (typeof val === 'object' && val !== null) {
        if (seen.has(val)) {
          return '[Circular]';
        }
        seen.add(val);
      }
      return val;
    },
    space
  );
}

// Test with circular reference
const obj: Record<string, unknown> = { name: 'Alice' };
obj.self = obj;

console.log(safeStringify(obj, 2));
/*
{
  "name": "Alice",
  "self": "[Circular]"
}
*/

Exercise 3: Date-Aware Parser

Create a parser that automatically converts ISO date strings to Date objects:

function parseWithDates<T>(json: string): T {}

// Test
const json = '{"event": "Meeting", "when": "2024-03-15T10:00:00.000Z"}';
const result = parseWithDates<{ event: string; when: Date }>(json);
console.log(result.when instanceof Date); // Should be true
Solution
function parseWithDates<T>(json: string): T {
  return JSON.parse(json, (key, value) => {
    // Match ISO 8601 date format
    if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
      const date = new Date(value);
      // Verify it's a valid date
      if (!isNaN(date.getTime())) {
        return date;
      }
    }
    return value;
  });
}

// Test
const json = '{"event": "Meeting", "when": "2024-03-15T10:00:00.000Z"}';
const result = parseWithDates<{ event: string; when: Date }>(json);

console.log(result.when instanceof Date); // true
console.log(result.when.getFullYear()); // 2024

Exercise 4: Error-Safe API Fetch

Create a function that fetches JSON from a URL with proper error handling:

interface FetchResult<T> {
  success: boolean;
  data?: T;
  error?: string;
}

async function safeFetchJson<T>(url: string): Promise<FetchResult<T>> {
  // Handle network errors
  // Handle non-OK responses
  // Handle invalid JSON
}
Solution
interface FetchResult<T> {
  success: boolean;
  data?: T;
  error?: string;
}

async function safeFetchJson<T>(url: string): Promise<FetchResult<T>> {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      return {
        success: false,
        error: `HTTP error: ${response.status} ${response.statusText}`,
      };
    }

    const text = await response.text();

    try {
      const data = JSON.parse(text) as T;
      return { success: true, data };
    } catch {
      return {
        success: false,
        error: 'Response is not valid JSON',
      };
    }
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Network error',
    };
  }
}

// Usage
interface Post {
  id: number;
  title: string;
}

const result = await safeFetchJson<Post>('https://jsonplaceholder.typicode.com/posts/1');

if (result.success) {
  console.log('Post title:', result.data?.title);
} else {
  console.error('Failed:', result.error);
}

Key Takeaways

  1. JSON.parse() converts JSON strings to JavaScript objects
  2. JSON.stringify() converts JavaScript objects to JSON strings
  3. JSON.parse() returns any - use type assertions or validation for type safety
  4. Always wrap JSON.parse() in try/catch - invalid JSON throws SyntaxError
  5. Dates become strings when serialized - use a reviver to restore them
  6. undefined, functions, and symbols are silently removed during serialization
  7. Circular references cause JSON.stringify() to throw
  8. Use replacer to filter properties and reviver to transform values

Resources

Resource Type Level
MDN: JSON Documentation Beginner
MDN: JSON.parse() Documentation Beginner
MDN: JSON.stringify() Documentation Beginner
JSON.org Specification Beginner

Next Lesson

Now that you can parse JSON data, let us learn how to transform it using powerful array methods.

Continue to Lesson 5.2: Data Transformation