Working with JSON
JSON (JavaScript Object Notation) is the standard format for exchanging data between web applications. JavaScript provides built-in methods to convert between JavaScript objects and JSON strings, making it essential for modern web development.
Understanding JSON
JSON is a text-based format that looks similar to JavaScript objects but with stricter rules:
// Valid JSON
{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"isActive": true,
"hobbies": ["reading", "coding", "gaming"],
"address": {
"city": "New York",
"zipCode": "10001"
}
}
// JSON Rules:
// ✓ Property names MUST be in double quotes
// ✓ Strings MUST use double quotes (no single quotes)
// ✓ No trailing commas
// ✓ No comments allowed
// ✓ No undefined, functions, or symbols
// ✓ Numbers, booleans, null, strings, arrays, and objects only
Key Difference: JavaScript objects are more flexible than JSON. JSON is a subset of JavaScript with stricter syntax rules designed for data interchange.
JSON.stringify() - Converting to JSON
JSON.stringify() converts JavaScript values to JSON strings:
const user = {
name: "John Doe",
age: 30,
email: "john@example.com",
hobbies: ["reading", "coding"]
};
// Basic conversion
const jsonString = JSON.stringify(user);
console.log(jsonString);
// {"name":"John Doe","age":30,"email":"john@example.com","hobbies":["reading","coding"]}
// Pretty printing with indentation (2 spaces)
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
/*
{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"hobbies": [
"reading",
"coding"
]
}
*/
// Using tabs for indentation
const tabbedJson = JSON.stringify(user, null, "\t");
JSON.parse() - Parsing JSON Strings
JSON.parse() converts JSON strings back to JavaScript objects:
const jsonString = '{"name":"John","age":30,"isActive":true}';
// Parse JSON string
const user = JSON.parse(jsonString);
console.log(user.name); // "John"
console.log(user.age); // 30
console.log(user.isActive); // true
// Parse array JSON
const arrayJson = '["apple", "banana", "orange"]';
const fruits = JSON.parse(arrayJson);
console.log(fruits[0]); // "apple"
Common Error: JSON.parse() throws a SyntaxError if the string is not valid JSON. Always use try/catch when parsing JSON from external sources!
Handling JSON Errors
Always wrap JSON.parse() in try/catch to handle invalid JSON gracefully:
function safeJsonParse(jsonString, fallback = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error("JSON parse error:", error.message);
return fallback;
}
}
// Usage
const validJson = '{"name":"John"}';
const invalidJson = '{name: "John"}'; // Invalid: no quotes around key
const user1 = safeJsonParse(validJson);
console.log(user1); // { name: "John" }
const user2 = safeJsonParse(invalidJson, { name: "Default" });
console.log(user2); // { name: "Default" }
// More detailed error handling
function parseJsonWithDetails(jsonString) {
try {
return {
success: true,
data: JSON.parse(jsonString),
error: null
};
} catch (error) {
return {
success: false,
data: null,
error: error.message
};
}
}
const result = parseJsonWithDetails(invalidJson);
if (result.success) {
console.log("Data:", result.data);
} else {
console.log("Error:", result.error);
}
The Replacer Parameter
JSON.stringify() accepts a replacer function or array to control what gets included:
const user = {
name: "John",
password: "secret123",
email: "john@example.com",
age: 30
};
// Using array to select specific properties
const limitedJson = JSON.stringify(user, ["name", "email"]);
console.log(limitedJson);
// {"name":"John","email":"john@example.com"}
// Using function to filter/transform values
const filteredJson = JSON.stringify(user, (key, value) => {
// Exclude password field
if (key === "password") {
return undefined;
}
// Convert age to string
if (key === "age") {
return String(value);
}
return value;
});
console.log(filteredJson);
// {"name":"John","email":"john@example.com","age":"30"}
The Reviver Parameter
JSON.parse() accepts a reviver function to transform parsed values:
const jsonString = '{
"name": "John",
"birthDate": "1990-01-15",
"lastLogin": "2024-01-20T10:30:00.000Z"
}';
// Parse with reviver to convert date strings to Date objects
const user = JSON.parse(jsonString, (key, value) => {
// Check if value looks like a date string
if (typeof value === "string") {
const datePattern = /^\d{4}-\d{2}-\d{2}/;
if (datePattern.test(value)) {
return new Date(value);
}
}
return value;
});
console.log(user.birthDate instanceof Date); // true
console.log(user.birthDate.getFullYear()); // 1990
console.log(user.lastLogin instanceof Date); // true
JSON with Special Types
Some JavaScript types need special handling when converting to/from JSON:
// Dates
const data1 = { date: new Date() };
const json1 = JSON.stringify(data1);
console.log(json1); // {"date":"2024-01-20T10:30:00.000Z"}
const parsed1 = JSON.parse(json1);
console.log(typeof parsed1.date); // "string" (not Date!)
console.log(new Date(parsed1.date)); // Convert back to Date
// undefined values are omitted
const data2 = { name: "John", age: undefined, email: "john@example.com" };
console.log(JSON.stringify(data2));
// {"name":"John","email":"john@example.com"}
// Functions are omitted
const data3 = { name: "John", greet: function() { return "Hi"; } };
console.log(JSON.stringify(data3));
// {"name":"John"}
// NaN and Infinity become null
const data4 = { value: NaN, infinite: Infinity };
console.log(JSON.stringify(data4));
// {"value":null,"infinite":null}
// Arrays keep undefined as null
const arr = [1, undefined, 3];
console.log(JSON.stringify(arr));
// [1,null,3]
Best Practice: When working with dates in JSON, always store them as ISO 8601 strings and convert them back to Date objects after parsing using a reviver function.
Deep Cloning with JSON
JSON can be used to create deep copies of objects (with limitations):
const original = {
name: "John",
hobbies: ["reading", "coding"],
address: {
city: "New York"
}
};
// Deep clone using JSON
const clone = JSON.parse(JSON.stringify(original));
// Modify clone
clone.hobbies.push("gaming");
clone.address.city = "Boston";
console.log(original.hobbies); // ["reading", "coding"] - unchanged!
console.log(original.address.city); // "New York" - unchanged!
console.log(clone.hobbies); // ["reading", "coding", "gaming"]
console.log(clone.address.city); // "Boston"
// ⚠️ Limitations:
const complexObject = {
date: new Date(),
regex: /test/,
func: () => {},
undef: undefined,
symbol: Symbol("test"),
circularRef: null
};
complexObject.circularRef = complexObject;
// This loses date, regex, function, undefined, symbol
// And throws error on circular reference!
// const cloneComplex = JSON.parse(JSON.stringify(complexObject)); // Error!
Warning: JSON cloning only works for simple data types (strings, numbers, booleans, null, plain objects, and arrays). It loses functions, dates (converts to strings), undefined, symbols, and throws errors on circular references.
Real-World JSON Patterns
// 1. API Response handling
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const jsonText = await response.text(); // Get as text first
// Check if response is valid JSON
if (!jsonText) {
throw new Error("Empty response");
}
const userData = JSON.parse(jsonText);
return userData;
} catch (error) {
console.error("Failed to fetch user:", error);
throw error;
}
}
// 2. LocalStorage with JSON
const storage = {
set(key, value) {
try {
const jsonString = JSON.stringify(value);
localStorage.setItem(key, jsonString);
} catch (error) {
console.error("Failed to save to localStorage:", error);
}
},
get(key, fallback = null) {
try {
const jsonString = localStorage.getItem(key);
return jsonString ? JSON.parse(jsonString) : fallback;
} catch (error) {
console.error("Failed to read from localStorage:", error);
return fallback;
}
},
remove(key) {
localStorage.removeItem(key);
}
};
// Usage
storage.set("user", { name: "John", age: 30 });
const user = storage.get("user");
console.log(user); // { name: "John", age: 30 }
// 3. Form data to JSON
function formToJson(formElement) {
const formData = new FormData(formElement);
const jsonObject = {};
formData.forEach((value, key) => {
// Handle multiple values (like checkboxes)
if (jsonObject[key]) {
if (!Array.isArray(jsonObject[key])) {
jsonObject[key] = [jsonObject[key]];
}
jsonObject[key].push(value);
} else {
jsonObject[key] = value;
}
});
return jsonObject;
}
// Usage
const form = document.querySelector("#myForm");
const formJson = JSON.stringify(formToJson(form));
console.log(formJson);
JSON Validation
// Check if string is valid JSON
function isValidJson(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
console.log(isValidJson('{"name":"John"}')); // true
console.log(isValidJson('{name: "John"}')); // false
// Validate JSON structure
function validateUserJson(jsonString) {
try {
const user = JSON.parse(jsonString);
// Check required fields
if (!user.name || typeof user.name !== "string") {
throw new Error("Invalid or missing name");
}
if (!user.email || !user.email.includes("@")) {
throw new Error("Invalid or missing email");
}
if (user.age && (typeof user.age !== "number" || user.age < 0)) {
throw new Error("Invalid age");
}
return { valid: true, data: user };
} catch (error) {
return { valid: false, error: error.message };
}
}
// Usage
const result1 = validateUserJson('{"name":"John","email":"john@example.com","age":30}');
console.log(result1); // { valid: true, data: {...} }
const result2 = validateUserJson('{"name":"John","email":"invalid"}');
console.log(result2); // { valid: false, error: "Invalid or missing email" }
Practice Exercise:
Task: Create a utility class for safely working with JSON that includes error handling and type conversion.
// Your task: Implement this class
class JsonUtil {
static stringify(value, options = {}) {
// Convert to JSON with error handling
// Support pretty printing option
}
static parse(jsonString, options = {}) {
// Parse JSON with error handling
// Support reviver for date conversion
// Support default fallback value
}
static isValid(str) {
// Check if string is valid JSON
}
static clone(obj) {
// Deep clone object using JSON
// Handle errors
}
}
Solution:
class JsonUtil {
static stringify(value, options = {}) {
const { pretty = false, space = 2 } = options;
try {
return JSON.stringify(value, null, pretty ? space : 0);
} catch (error) {
console.error("JSON stringify error:", error.message);
return null;
}
}
static parse(jsonString, options = {}) {
const { convertDates = false, fallback = null } = options;
try {
const reviver = convertDates
? (key, value) => {
if (typeof value === "string") {
const datePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
if (datePattern.test(value)) {
return new Date(value);
}
}
return value;
}
: undefined;
return JSON.parse(jsonString, reviver);
} catch (error) {
console.error("JSON parse error:", error.message);
return fallback;
}
}
static isValid(str) {
if (typeof str !== "string") return false;
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
static clone(obj) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("JSON clone error:", error.message);
return null;
}
}
}
// Usage
const obj = { name: "John", date: new Date().toISOString() };
const json = JsonUtil.stringify(obj, { pretty: true });
console.log(json);
const parsed = JsonUtil.parse(json, { convertDates: true });
console.log(parsed.date instanceof Date); // true
console.log(JsonUtil.isValid('{"name":"John"}')); // true
console.log(JsonUtil.isValid('{invalid}')); // false
const cloned = JsonUtil.clone(obj);
console.log(cloned);
Summary
In this lesson, you learned:
- JSON is a text format for data interchange with strict syntax rules
- JSON.stringify() converts JavaScript to JSON strings
- JSON.parse() converts JSON strings to JavaScript objects
- Always use try/catch when parsing JSON from external sources
- Replacer and reviver functions provide transformation control
- Special types (dates, functions, undefined) need careful handling
- JSON can create deep clones but has limitations
- Validate JSON structure for robust applications
Next Up: In the next lesson, we'll explore advanced async patterns including async iteration, generators, and sophisticated error handling strategies!