JavaScript JSON Serialization Guide

JavaScript ~4 min read
JavaScriptJSONSerializationJSON.stringifyJSON.parse

Detailed guide to JSON.stringify and JSON.parse advanced usage, including replacer/reviver parameters, BigInt handling, deep copy pitfalls, and JSON Schema validation.

JSON.stringify() Serialization Rules

JSON.stringify() is the core method in JavaScript for converting objects to JSON strings. Understanding its serialization rules is essential for avoiding unexpected behavior.

const data = {
  name: "Alice",
  age: 30,
  active: true,
  score: null,
  greet() { return "hi"; },
  [Symbol("id")]: 123,
  undef: undefined,
};

console.log(JSON.stringify(data, null, 2));
// {
//   "name": "Alice",
//   "age": 30,
//   "active": true,
//   "score": null
// }

Key rules:

  • Functions and undefined values are silently omitted
  • Symbol keys are silently omitted
  • null is preserved as JSON null
  • NaN and Infinity become null

replacer Parameter

The replacer parameter can be either a function or an array, providing two completely different filtering mechanisms.

// Function replacer: called for each key-value pair
JSON.stringify(data, (key, value) => {
  if (typeof value === "function") return undefined;
  return value;
});

// Array replacer: only keep listed keys
JSON.stringify(data, ["name", "age"]);
// '{"name":"Alice","age":30}'

toJSON() Method

If an object defines a toJSON() method, JSON.stringify() will call it and serialize its return value instead.

const user = {
  name: "Alice",
  password: "secret123",
  toJSON() {
    return { name: this.name }; // Exclude password
  },
};

console.log(JSON.stringify(user)); // '{"name":"Alice"}'

JSON.parse() and reviver

The reviver function allows custom transformation during deserialization.

const jsonStr = '{"name":"Alice","created_at":"2026-07-15T10:30:00Z"}';

const result = JSON.parse(jsonStr, (key, value) => {
  if (key.endsWith("_at") && typeof value === "string") {
    return new Date(value);
  }
  return value;
});

console.log(result.created_at); // Date object

Deep Clone Pitfalls

Using JSON.parse(JSON.stringify(obj)) for deep cloning is a common pattern but has significant limitations.

const obj = {
  date: new Date(),
  regex: /test/gi,
  undef: undefined,
  nan: NaN,
  infinity: Infinity,
  map: new Map([["key", "value"]]),
  set: new Set([1, 2, 3]),
};

const clone = JSON.parse(JSON.stringify(obj));
// date: string (not Date!)
// regex: {} (empty object!)
// undef: missing entirely
// nan: null
// infinity: null
// map: {} (empty object!)
// set: {} (empty object!)

For reliable deep cloning, use structuredClone() (modern browsers) or libraries like lodash.

Circular Reference Handling

JSON.stringify throws TypeError on circular references:

const obj = {};
obj.self = obj;
// JSON.stringify(obj); // TypeError: Converting circular structure

// Solution: use a replacer with WeakSet
function stringifySafe(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

BigInt Handling

BigInt values cannot be directly serialized with JSON.stringify() — it throws a TypeError.

const data = { id: 9007199254740993n };
// JSON.stringify(data); // TypeError: Do not know how to serialize a BigInt

// Solution: use replacer
const json = JSON.stringify(data, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
);
// '{"id":"9007199254740993"}'

// Note: After round-trip, id is a string, not BigInt.
// Use a reviver to restore:
const parsed = JSON.parse(json, (key, value) => {
  if (key === 'id' && typeof value === 'string') {
    return BigInt(value);
  }
  return value;
});

Third-Party Libraries

For advanced JSON operations, consider these libraries:

  • superjson: Supports Date, Map, Set, BigInt, RegExp, and more
  • devalue: Fast, safe serialization with support for circular references
  • flatted: Handles circular references by tracking object identity
import superjson from "superjson";

const data = {
  date: new Date(),
  map: new Map([["key", "value"]]),
};

const { json, meta } = superjson.serialize(data);
// json: {"date":"2026-07-15T...","map":[["key","value"]]}
// meta: {"values":{"date":["Date"],"map":["Map"]}}

const restored = superjson.deserialize({ json, meta });
// Perfect round-trip with types preserved

Fetch API Integration

When working with REST APIs, JSON is the standard data format:

// Fetching JSON data
async function getUsers() {
  const response = await fetch('/api/users');
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json(); // Automatically parses JSON
}

// Sending JSON data
async function createUser(userData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });
  return response.json();
}

LocalStorage Persistence

JSON serialization is essential for browser storage:

// Save to localStorage
function saveState(key, state) {
  try {
    localStorage.setItem(key, JSON.stringify(state));
  } catch (e) {
    console.error('Failed to save state:', e);
  }
}

// Load from localStorage
function loadState(key, defaultValue = null) {
  try {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : defaultValue;
  } catch (e) {
    console.error('Failed to load state:', e);
    return defaultValue;
  }
}

// Usage
const settings = { theme: 'dark', language: 'en' };
saveState('app-settings', settings);
const loaded = loadState('app-settings');

Stream Processing

For large JSON payloads, use ReadableStream:

async function processLargeJSON(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  let buffer = '';
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    // Process chunks as needed
  }

  return JSON.parse(buffer);
}

JSON Schema Validation

Use ajv for runtime JSON Schema validation:

import Ajv from 'ajv';

const ajv = new Ajv();

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    age: { type: 'integer', minimum: 0 },
    email: { type: 'string', format: 'email' }
  },
  required: ['name', 'age']
};

const validate = ajv.compile(schema);

const data = { name: 'Alice', age: 30 };
if (validate(data)) {
  console.log('Valid!');
} else {
  console.log('Errors:', validate.errors);
}

JSON API Comparison Across Languages

Language Library Serialize Deserialize Pretty Print Config
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions
TypeScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Rust serde_json serde_json::to_string(&obj) serde_json::from_str::<T>(str) serde_json::to_string_pretty(&obj) serde attributes, custom (de)serializers

Frequently Asked Questions (FAQ)

How to handle datetime types in JSON serialization?

Most language JSON libraries do not support datetime types by default. The common approach is to serialize as ISO 8601 format strings (e.g. "2026-07-15T10:30:00Z") or Unix timestamps, then convert back to datetime objects during deserialization.

How to ignore null values or empty fields in JSON serialization?

Different languages have different implementations: Python can filter None values in custom encoders; JavaScript can use replacer functions; Java Jackson uses @JsonInclude annotations; Go uses omitempty struct tags; C# sets DefaultIgnoreCondition.

How to handle circular references in JSON serialization?

Circular references are a common pitfall. Standard JSON libraries usually throw exceptions or cause stack overflow. Solutions include using @JsonIdentityInfo (Java Jackson), implementing custom serializers, or converting to DTOs before serialization.

Why are my private fields not being serialized?

JSON serialization libraries typically only serialize public fields or properties with getters. This is a security consideration. You can use annotations like @JsonProperty (Java Jackson) or [JsonInclude] (C#) to include non-public members.

What are the performance optimization tips for JSON serialization?

1) Reuse serializer instances; 2) Use streaming APIs for large data; 3) Use compile-time generation (C# Source Generator, Go easyjson); 4) Avoid excessive nesting; 5) Use database pagination for large JSON.

Recommended Tools

These tools can help you handle JSON data more efficiently in JavaScript development:

Related Articles