Chapter 11: JavaScript JSON - Complete Guide to Data Serialization
JSON (JavaScript Object Notation) is a lightweight data interchange format that has become the standard for data exchange in web applications. Understanding JSON is essential for working with APIs, storing data, and communicating between different systems.
Why JSON is Essential in JavaScript
JSON in JavaScript is crucial because it:
- Enables Data Exchange: Standard format for API communication and data transfer
- Supports Web APIs: Primary format for REST APIs and web services
- Facilitates Storage: Used for localStorage, sessionStorage, and database storage
- Enables Configuration: Common format for configuration files and settings
- Supports Serialization: Converts JavaScript objects to strings and back
- Ensures Compatibility: Works across different programming languages and platforms
Learning Objectives
Through this chapter, you will master:
- JSON syntax and data types
- JSON.stringify() and JSON.parse() methods
- Custom serialization and parsing
- JSON validation and error handling
- Working with nested objects and arrays
- Performance considerations and best practices
- Real-world JSON usage patterns
JSON Basics
JSON Syntax
// JSON data types
const jsonExamples = {
// String
string: "Hello, World!",
// Number
number: 42,
float: 3.14,
// Boolean
boolean: true,
// Null
nullValue: null,
// Array
array: [1, 2, 3, "four", true],
// Object
object: {
name: "John",
age: 30,
city: "New York"
},
// Nested structures
nested: {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
],
settings: {
theme: "dark",
notifications: true
}
}
};
// Valid JSON string
const validJsonString = `{
"name": "John Doe",
"age": 30,
"isActive": true,
"hobbies": ["reading", "coding", "gaming"],
"address": {
"street": "123 Main St",
"city": "New York",
"zipCode": "10001"
}
}`;
// Invalid JSON examples
const invalidJsonExamples = [
// Missing quotes around keys
'{ name: "John", age: 30 }',
// Single quotes instead of double quotes
"{ 'name': 'John', 'age': 30 }",
// Trailing comma
'{ "name": "John", "age": 30, }',
// Undefined value
'{ "name": "John", "age": undefined }',
// Function
'{ "name": "John", "greet": function() { return "Hello"; } }'
];
JSON.stringify()
// Basic JSON.stringify()
const obj = {
name: "John Doe",
age: 30,
isActive: true,
hobbies: ["reading", "coding"],
address: {
street: "123 Main St",
city: "New York"
}
};
// Convert to JSON string
const jsonString = JSON.stringify(obj);
console.log(jsonString);
// {"name":"John Doe","age":30,"isActive":true,"hobbies":["reading","coding"],"address":{"street":"123 Main St","city":"New York"}}
// Pretty printing with indentation
const prettyJson = JSON.stringify(obj, null, 2);
console.log(prettyJson);
/*
{
"name": "John Doe",
"age": 30,
"isActive": true,
"hobbies": [
"reading",
"coding"
],
"address": {
"street": "123 Main St",
"city": "New York"
}
}
*/
// Custom indentation
const customIndent = JSON.stringify(obj, null, 4);
console.log(customIndent);
JSON.parse()
// Basic JSON.parse()
const jsonString = '{"name":"John Doe","age":30,"isActive":true}';
try {
const parsedObj = JSON.parse(jsonString);
console.log(parsedObj);
// { name: "John Doe", age: 30, isActive: true }
} catch (error) {
console.error('JSON parse error:', error.message);
}
// Parse with error handling
function safeJsonParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('Invalid JSON:', error.message);
return defaultValue;
}
}
// Usage
const validJson = '{"name":"John"}';
const invalidJson = '{name:"John"}';
console.log(safeJsonParse(validJson)); // { name: "John" }
console.log(safeJsonParse(invalidJson)); // null
console.log(safeJsonParse(invalidJson, {})); // {}
Advanced JSON Operations
Custom Serialization with Replacer
// Custom replacer function
const user = {
name: "John Doe",
age: 30,
password: "secret123",
email: "[email protected]",
lastLogin: new Date(),
profile: {
bio: "Software developer",
avatar: "avatar.jpg"
}
};
// Exclude sensitive data
const publicUser = JSON.stringify(user, (key, value) => {
// Exclude password
if (key === 'password') {
return undefined;
}
// Convert Date to ISO string
if (value instanceof Date) {
return value.toISOString();
}
// Include only specific keys
const allowedKeys = ['name', 'age', 'email', 'profile'];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const filtered = {};
allowedKeys.forEach(key => {
if (key in value) {
filtered[key] = value[key];
}
});
return filtered;
}
return value;
});
console.log(publicUser);
// Array of keys to include
const limitedUser = JSON.stringify(user, ['name', 'age', 'email']);
console.log(limitedUser); // {"name":"John Doe","age":30,"email":"[email protected]"}
Custom Parsing with Reviver
// Custom reviver function
const jsonWithDates = `{
"name": "John Doe",
"createdAt": "2023-01-15T10:30:00.000Z",
"updatedAt": "2023-12-01T15:45:00.000Z",
"settings": {
"theme": "dark",
"notifications": true
}
}`;
const parsedWithDates = JSON.parse(jsonWithDates, (key, value) => {
// Convert ISO date strings back to Date objects
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) {
return new Date(value);
}
// Transform specific keys
if (key === 'name') {
return value.toUpperCase();
}
return value;
});
console.log(parsedWithDates);
console.log(parsedWithDates.createdAt instanceof Date); // true
Handling Special Values
// JSON.stringify with special values
const objWithSpecialValues = {
normalString: "Hello",
undefinedValue: undefined,
functionValue: function() { return "Hello"; },
symbolValue: Symbol('test'),
dateValue: new Date('2023-01-01'),
regexValue: /test/gi,
infinityValue: Infinity,
nanValue: NaN
};
// Default behavior
console.log(JSON.stringify(objWithSpecialValues));
// {"normalString":"Hello","dateValue":"2023-01-01T00:00:00.000Z","infinityValue":null,"nanValue":null}
// Custom handling of special values
const customStringify = JSON.stringify(objWithSpecialValues, (key, value) => {
if (typeof value === 'function') {
return '[Function]';
}
if (typeof value === 'symbol') {
return '[Symbol]';
}
if (value instanceof RegExp) {
return value.toString();
}
if (value === Infinity) {
return 'Infinity';
}
if (Number.isNaN(value)) {
return 'NaN';
}
if (value === undefined) {
return '[Undefined]';
}
return value;
});
console.log(customStringify);
JSON Validation
Basic Validation
// JSON validation function
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (error) {
return false;
}
}
// Enhanced validation with error details
function validateJSON(str) {
try {
JSON.parse(str);
return { valid: true, error: null };
} catch (error) {
return {
valid: false,
error: error.message,
position: error.message.match(/position (\d+)/)?.[1]
};
}
}
// Usage
const validJson = '{"name":"John","age":30}';
const invalidJson = '{name:"John",age:30}';
console.log(isValidJSON(validJson)); // true
console.log(isValidJSON(invalidJson)); // false
const validation = validateJSON(invalidJson);
console.log(validation);
// { valid: false, error: "Expected property name or '}' in JSON at position 1", position: "1" }
Schema Validation
// Simple schema validation
function validateSchema(data, schema) {
const errors = [];
for (const [key, rules] of Object.entries(schema)) {
const value = data[key];
// Required field check
if (rules.required && (value === undefined || value === null)) {
errors.push(`${key} is required`);
continue;
}
// Type check
if (value !== undefined && rules.type && typeof value !== rules.type) {
errors.push(`${key} must be of type ${rules.type}`);
}
// String length check
if (rules.type === 'string' && rules.minLength && value.length < rules.minLength) {
errors.push(`${key} must be at least ${rules.minLength} characters long`);
}
if (rules.type === 'string' && rules.maxLength && value.length > rules.maxLength) {
errors.push(`${key} must be no more than ${rules.maxLength} characters long`);
}
// Number range check
if (rules.type === 'number' && rules.min !== undefined && value < rules.min) {
errors.push(`${key} must be at least ${rules.min}`);
}
if (rules.type === 'number' && rules.max !== undefined && value > rules.max) {
errors.push(`${key} must be no more than ${rules.max}`);
}
// Array length check
if (rules.type === 'array' && rules.minItems && value.length < rules.minItems) {
errors.push(`${key} must have at least ${rules.minItems} items`);
}
// Custom validation
if (rules.validate && !rules.validate(value)) {
errors.push(`${key} failed custom validation`);
}
}
return {
valid: errors.length === 0,
errors
};
}
// Usage
const userSchema = {
name: { type: 'string', required: true, minLength: 2, maxLength: 50 },
age: { type: 'number', required: true, min: 0, max: 120 },
email: {
type: 'string',
required: true,
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
},
hobbies: { type: 'array', minItems: 1 }
};
const userData = {
name: "John",
age: 30,
email: "[email protected]",
hobbies: ["reading", "coding"]
};
const validation = validateSchema(userData, userSchema);
console.log(validation);
// { valid: true, errors: [] }
const invalidData = {
name: "J",
age: 150,
email: "invalid-email"
};
const invalidValidation = validateSchema(invalidData, userSchema);
console.log(invalidValidation);
// { valid: false, errors: ["name must be at least 2 characters long", "age must be no more than 120", "email failed custom validation", "hobbies is required"] }
Working with Nested Data
Deep Object Manipulation
// Deep merge objects
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
// Usage
const baseConfig = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
},
ui: {
theme: 'light'
}
};
const userConfig = {
api: {
timeout: 10000
},
ui: {
theme: 'dark',
language: 'en'
}
};
const mergedConfig = deepMerge(baseConfig, userConfig);
console.log(JSON.stringify(mergedConfig, null, 2));
// Deep clone with JSON
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Flatten nested object
function flattenObject(obj, prefix = '') {
const flattened = {};
for (const key in obj) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
Object.assign(flattened, flattenObject(obj[key], newKey));
} else {
flattened[newKey] = obj[key];
}
}
return flattened;
}
// Usage
const nestedObj = {
user: {
profile: {
name: "John",
settings: {
theme: "dark"
}
},
preferences: ["reading", "coding"]
}
};
const flattened = flattenObject(nestedObj);
console.log(flattened);
// { "user.profile.name": "John", "user.profile.settings.theme": "dark", "user.preferences": ["reading", "coding"] }
Array Operations with JSON
// Filter and transform arrays
const users = [
{ id: 1, name: "John", age: 30, active: true },
{ id: 2, name: "Jane", age: 25, active: false },
{ id: 3, name: "Bob", age: 35, active: true }
];
// Filter active users and serialize
const activeUsers = users
.filter(user => user.active)
.map(user => ({ id: user.id, name: user.name }));
const activeUsersJson = JSON.stringify(activeUsers, null, 2);
console.log(activeUsersJson);
// Group by property
function groupBy(array, key) {
return array.reduce((groups, item) => {
const group = item[key];
groups[group] = groups[group] || [];
groups[group].push(item);
return groups;
}, {});
}
const groupedByActive = groupBy(users, 'active');
console.log(JSON.stringify(groupedByActive, null, 2));
// Sort and serialize
const sortedUsers = users.sort((a, b) => a.age - b.age);
const sortedUsersJson = JSON.stringify(sortedUsers, null, 2);
console.log(sortedUsersJson);
Performance Considerations
Large JSON Handling
// Streaming JSON parser for large files
class StreamingJSONParser {
constructor() {
this.buffer = '';
this.depth = 0;
this.inString = false;
this.escapeNext = false;
}
parseChunk(chunk) {
this.buffer += chunk;
const results = [];
let i = 0;
while (i < this.buffer.length) {
const char = this.buffer[i];
if (this.escapeNext) {
this.escapeNext = false;
i++;
continue;
}
if (char === '\\' && this.inString) {
this.escapeNext = true;
i++;
continue;
}
if (char === '"' && !this.escapeNext) {
this.inString = !this.inString;
}
if (!this.inString) {
if (char === '{' || char === '[') {
this.depth++;
} else if (char === '}' || char === ']') {
this.depth--;
if (this.depth === 0) {
// Complete object found
const completeObject = this.buffer.substring(0, i + 1);
try {
const parsed = JSON.parse(completeObject);
results.push(parsed);
this.buffer = this.buffer.substring(i + 1);
i = 0;
continue;
} catch (error) {
// Continue parsing
}
}
}
}
i++;
}
return results;
}
}
// Usage example
const parser = new StreamingJSONParser();
const chunks = [
'{"name":"John","age":30}{"name":"Jane","age":25}',
'{"name":"Bob","age":35}'
];
chunks.forEach(chunk => {
const results = parser.parseChunk(chunk);
results.forEach(obj => console.log('Parsed:', obj));
});
Memory Optimization
// Efficient JSON processing for large datasets
function processLargeJSON(jsonString, processor) {
const objects = [];
let currentObject = '';
let depth = 0;
let inString = false;
let escapeNext = false;
for (let i = 0; i < jsonString.length; i++) {
const char = jsonString[i];
if (escapeNext) {
escapeNext = false;
currentObject += char;
continue;
}
if (char === '\\' && inString) {
escapeNext = true;
currentObject += char;
continue;
}
if (char === '"' && !escapeNext) {
inString = !inString;
}
currentObject += char;
if (!inString) {
if (char === '{' || char === '[') {
depth++;
} else if (char === '}' || char === ']') {
depth--;
if (depth === 0) {
try {
const obj = JSON.parse(currentObject);
const processed = processor(obj);
if (processed) {
objects.push(processed);
}
} catch (error) {
console.error('Parse error:', error);
}
currentObject = '';
}
}
}
}
return objects;
}
// Usage
const largeJsonString = '{"id":1,"data":"large"}{"id":2,"data":"dataset"}{"id":3,"data":"processing"}';
const processed = processLargeJSON(largeJsonString, (obj) => {
// Only keep objects with even IDs
return obj.id % 2 === 0 ? obj : null;
});
console.log(processed);
Best Practices
1. Error Handling
// Robust JSON handling with error recovery
class JSONHandler {
static safeStringify(obj, replacer = null, space = null) {
try {
return JSON.stringify(obj, replacer, space);
} catch (error) {
console.error('JSON stringify error:', error);
return null;
}
}
static safeParse(str, reviver = null) {
try {
return JSON.parse(str, reviver);
} catch (error) {
console.error('JSON parse error:', error);
return null;
}
}
static validateAndParse(str, schema = null) {
const parsed = this.safeParse(str);
if (!parsed) {
return { success: false, data: null, error: 'Invalid JSON' };
}
if (schema) {
const validation = validateSchema(parsed, schema);
if (!validation.valid) {
return { success: false, data: parsed, error: validation.errors };
}
}
return { success: true, data: parsed, error: null };
}
}
// Usage
const result = JSONHandler.validateAndParse('{"name":"John","age":30}');
if (result.success) {
console.log('Valid JSON:', result.data);
} else {
console.error('Error:', result.error);
}
2. Local Storage Integration
// JSON-based local storage wrapper
class JSONStorage {
constructor(storage = localStorage) {
this.storage = storage;
}
setItem(key, value) {
try {
const jsonString = JSON.stringify(value);
this.storage.setItem(key, jsonString);
return true;
} catch (error) {
console.error('Storage set error:', error);
return false;
}
}
getItem(key, defaultValue = null) {
try {
const jsonString = this.storage.getItem(key);
if (jsonString === null) {
return defaultValue;
}
return JSON.parse(jsonString);
} catch (error) {
console.error('Storage get error:', error);
return defaultValue;
}
}
removeItem(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
// Batch operations
setItems(items) {
const results = {};
for (const [key, value] of Object.entries(items)) {
results[key] = this.setItem(key, value);
}
return results;
}
getItems(keys) {
const results = {};
for (const key of keys) {
results[key] = this.getItem(key);
}
return results;
}
}
// Usage
const jsonStorage = new JSONStorage();
// Store complex data
const userData = {
name: "John Doe",
preferences: {
theme: "dark",
language: "en"
},
history: [
{ action: "login", timestamp: new Date() },
{ action: "view", page: "dashboard", timestamp: new Date() }
]
};
jsonStorage.setItem('userData', userData);
const retrieved = jsonStorage.getItem('userData');
console.log(retrieved);
3. API Integration
// JSON-based API client
class JSONApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
console.error('API request error:', error);
throw error;
}
}
get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: data
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: data
});
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new JSONApiClient('https://api.example.com');
api.get('/users')
.then(users => console.log('Users:', users))
.catch(error => console.error('Error:', error));
api.post('/users', { name: 'John', email: '[email protected]' })
.then(user => console.log('Created user:', user))
.catch(error => console.error('Error:', error));
Summary
JSON is fundamental to modern web development:
- Basic Operations: Use JSON.stringify() and JSON.parse() for serialization
- Custom Handling: Implement replacer and reviver functions for special cases
- Validation: Validate JSON structure and data integrity
- Performance: Handle large JSON datasets efficiently
- Error Handling: Implement robust error handling and recovery
- Integration: Use JSON with storage, APIs, and data exchange
- Best Practices: Follow security and performance best practices
Mastering JSON enables you to work effectively with data in modern web applications, from simple configuration files to complex API integrations and data storage solutions.
This tutorial is part of the JavaScript Mastery series by syscook.dev