Lesson 5.1: JSON Parsing
Duration: 45 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Understand what JSON is and why it matters
- Parse JSON strings into JavaScript objects safely
- Convert JavaScript objects to JSON strings
- Handle JSON parsing errors gracefully
- 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
-
Keys must be double-quoted strings
{ "name": "Alice" } // Valid JSON { name: "Alice" } // Invalid JSON (unquoted key) { 'name': 'Alice' } // Invalid JSON (single quotes) -
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"}
- Strings (double quotes only):
-
Values cannot be:
- Functions
undefinedDateobjects (converted to strings)NaNorInfinity- 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
- JSON.parse() converts JSON strings to JavaScript objects
- JSON.stringify() converts JavaScript objects to JSON strings
JSON.parse()returnsany- use type assertions or validation for type safety- Always wrap JSON.parse() in try/catch - invalid JSON throws SyntaxError
- Dates become strings when serialized - use a reviver to restore them
- undefined, functions, and symbols are silently removed during serialization
- Circular references cause JSON.stringify() to throw
- 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.