JavaScript JSON Guide: JSON.parse(), JSON.stringify(), and Real-World Patterns

13 min read

JSON was born from JavaScript, and working with it in JS is as natural as it gets. But there are nuances — lost data during serialization, security pitfalls, performance traps — that catch even experienced developers. This guide covers everything from basics to production patterns.

JSON.parse(): String to Object

JSON.parse() takes a JSON string and returns a JavaScript value (object, array, string, number, boolean, or null).

// Basic parsing
const user = JSON.parse('{"name": "Alice", "age": 30}');
console.log(user.name); // "Alice"

// Parsing an array
const items = JSON.parse('[1, 2, 3]');
console.log(items[0]); // 1

// Parsing primitives
JSON.parse('"hello"');  // "hello"
JSON.parse('42');       // 42
JSON.parse('true');     // true
JSON.parse('null');     // null

The reviver parameter

JSON.parse() accepts an optional second argument — a function that transforms each value during parsing. This is essential for handling dates:

// Problem: dates come back as strings
const data = JSON.parse('{"created": "2026-04-27T10:00:00Z"}');
console.log(typeof data.created); // "string" — not a Date!

// Solution: use a reviver to convert date strings
const dateReviver = (key, value) => {
  if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
};

const data2 = JSON.parse('{"created": "2026-04-27T10:00:00Z"}', dateReviver);
console.log(data2.created instanceof Date); // true

JSON.stringify(): Object to String

JSON.stringify() converts a JavaScript value to a JSON string. It takes three arguments:

JSON.stringify(value, replacer, space)
//              ↑       ↑         ↑
//           the data  filter   indentation

Basic usage

const user = { name: "Alice", age: 30, active: true };

// Compact (for APIs and storage)
JSON.stringify(user);
// '{"name":"Alice","age":30,"active":true}'

// Pretty printed (for debugging and config files)
JSON.stringify(user, null, 2);
// {
//   "name": "Alice",
//   "age": 30,
//   "active": true
// }

The replacer parameter

The replacer filters or transforms values during serialization:

const user = {
  name: "Alice",
  password: "secret123",
  email: "alice@example.com",
  age: 30
};

// Array replacer: only include specific keys
JSON.stringify(user, ["name", "email"], 2);
// { "name": "Alice", "email": "alice@example.com" }

// Function replacer: transform or exclude values
JSON.stringify(user, (key, value) => {
  if (key === "password") return undefined; // exclude
  if (key === "name") return value.toUpperCase(); // transform
  return value;
}, 2);
// { "name": "ALICE", "email": "alice@example.com", "age": 30 }

Tip: Need to quickly format or minify JSON output? Paste it into our JSON Formatter — it handles pretty printing, minification, and key sorting without writing code.

What JSON.stringify() Silently Drops

This is where most bugs come from. JSON.stringify() silently ignores or converts certain values:

const data = {
  fn: function() {},     // ❌ dropped (functions)
  sym: Symbol("id"),     // ❌ dropped (symbols)
  undef: undefined,      // ❌ dropped (undefined)
  nan: NaN,              // ⚠️ becomes null
  inf: Infinity,         // ⚠️ becomes null
  negInf: -Infinity,     // ⚠️ becomes null
  date: new Date(),      // ⚠️ becomes ISO string (can't parse back)
  regex: /abc/g,         // ⚠️ becomes {}
  map: new Map([["a", 1]]),  // ⚠️ becomes {}
  set: new Set([1, 2, 3]),   // ⚠️ becomes {}
  name: "Alice",         // ✅ kept
  count: 42,             // ✅ kept
};

console.log(JSON.stringify(data, null, 2));
// Only "name", "count", "nan" (null), "inf" (null), "negInf" (null),
// "date" (string), "regex" ({}), "map" ({}), "set" ({}) survive

Key takeaway: if you serialize and then parse, you won't get the same object back. Dates become strings, Maps/Sets become empty objects, and functions/symbols/undefined disappear entirely.

Getting parse errors? Paste the raw JSON into our JSON Validator to see the exact error location and fix suggestions. Common issues: trailing commas, single quotes, and unescaped characters.

Fetching JSON from APIs

The fetch() API is the modern way to make HTTP requests. Its .json() method parses the response body:

// GET request
async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return response.json(); // parses JSON body
}

// POST request with JSON body
async function createUser(userData) {
  const response = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

Defensive fetch wrapper

async function fetchJson(url, options = {}) {
  const response = await fetch(url, options);

  // Check content type before parsing
  const contentType = response.headers.get("content-type");
  if (!contentType?.includes("application/json")) {
    throw new Error(
      `Expected JSON, got ${contentType}. Status: ${response.status}`
    );
  }

  const data = await response.json();

  if (!response.ok) {
    throw Object.assign(new Error(data.message || "Request failed"), {
      status: response.status,
      data,
    });
  }

  return data;
}

JSON and localStorage

localStorage only stores strings. You must serialize to JSON when writing and parse when reading:

// Save to localStorage
const settings = { theme: "dark", fontSize: 16, sidebar: true };
localStorage.setItem("settings", JSON.stringify(settings));

// Read from localStorage
function getSettings() {
  const raw = localStorage.getItem("settings");
  if (!raw) return null;

  try {
    return JSON.parse(raw);
  } catch {
    // Corrupted data — clear it
    localStorage.removeItem("settings");
    return null;
  }
}

// Common mistake: forgetting to parse
const bad = localStorage.getItem("settings");
console.log(bad.theme); // undefined — bad is a string, not an object!

Deep Cloning with JSON

JSON.parse(JSON.stringify(obj)) is a quick way to deep clone objects — but it has limitations:

// Quick deep clone
const original = { a: 1, b: { c: 2, d: [3, 4] } };
const clone = JSON.parse(JSON.stringify(original));

clone.b.c = 99;
console.log(original.b.c); // 2 — original unchanged

// ⚠️ Limitations: loses functions, dates, undefined, Map, Set
// Use structuredClone() instead (modern browsers + Node 17+)
const betterClone = structuredClone(original);

The toJSON() Method

If an object has a toJSON() method, JSON.stringify() calls it automatically:

class User {
  constructor(name, email, password) {
    this.name = name;
    this.email = email;
    this.password = password;
  }

  // Control what gets serialized
  toJSON() {
    return {
      name: this.name,
      email: this.email,
      // password intentionally excluded
    };
  }
}

const user = new User("Alice", "alice@example.com", "secret");
JSON.stringify(user);
// '{"name":"Alice","email":"alice@example.com"}'
// password is not included

Error Handling Patterns

// Safe parse with fallback
function safeParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch {
    return fallback;
  }
}

safeParse('{"valid": true}');    // { valid: true }
safeParse('not json');           // null
safeParse('not json', {});       // {}

// Parse with error details
function parseWithError(text) {
  try {
    return { data: JSON.parse(text), error: null };
  } catch (e) {
    return { data: null, error: e.message };
  }
}

const result = parseWithError('{"trailing": "comma",}');
console.log(result.error);
// "Expected double-quoted property name in JSON at position 22"

Comparing API responses? Use our JSON Diff tool to see exactly what changed between two responses — additions, deletions, and modifications highlighted with full path tracking.

JSON in TypeScript

JSON.parse() returns any in TypeScript, which defeats type safety. Here's how to handle it:

// Define your type
interface User {
  name: string;
  age: number;
  email: string;
}

// Type assertion (trusts the data)
const user = JSON.parse(jsonString) as User;

// Runtime validation with a type guard (safer)
function isUser(data: unknown): data is User {
  return (
    typeof data === "object" && data !== null &&
    "name" in data && typeof (data as User).name === "string" &&
    "age" in data && typeof (data as User).age === "number" &&
    "email" in data && typeof (data as User).email === "string"
  );
}

const parsed = JSON.parse(jsonString);
if (isUser(parsed)) {
  console.log(parsed.name); // TypeScript knows this is a string
} else {
  console.error("Invalid user data");
}

// Best: use Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ name: z.string(), age: z.number(), email: z.string().email() });
// const user = UserSchema.parse(JSON.parse(jsonString));

Need TypeScript interfaces from JSON? Paste a sample API response into our JSON Converter and select "TypeScript" to auto-generate type definitions.

Performance Tips

  • Avoid parsing in hot loops. Parse once, pass the object around. JSON.parse() is fast but not free.
  • Use Response.json() over manual parse. fetch().then(r => r.json()) is optimized internally and avoids creating an intermediate string.
  • Stream large JSON. For Node.js, use libraries like stream-json or JSONStream to process large files without loading them entirely into memory.
  • Consider JSON.stringify() cost. Serializing large objects (10MB+) blocks the event loop. Move it to a Worker if needed.
  • Cache parsed results. If you parse the same config or schema multiple times, cache the parsed object.

Common Mistakes

  • Double parsing: JSON.parse(JSON.parse(str)) — this happens when a string is double-encoded on the server side.
  • Parsing an object: JSON.parse(obj) when obj is already an object. Check typeof first.
  • Missing await: const data = response.json() without await — you get a Promise, not the data.
  • Circular references: JSON.stringify() throws on circular objects. Use a replacer to handle them, or use libraries like flatted.
  • Assuming order: JSON object key order is not guaranteed by the spec, even though V8 preserves insertion order in practice.

Debug your JSON

Paste JSON from your JavaScript app into our tools to validate, format, and explore the structure.

We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies. Learn more