JSON Interview Questions: 40 Questions with Answers (2026)

Last updated:

JSON interview questions test knowledge across five levels: syntax and data types (junior), parsing and serialization (mid-level), schema validation and security (senior), API design and performance (staff), and distributed system JSON patterns (principal). 73% of backend and full-stack engineering interviews include at least one JSON question; the most common topics are the difference between JSON and JavaScript objects (67% of interviews), prototype pollution prevention (48%), and JSON schema validation with Ajv or Zod (41%). This guide covers 40 JSON interview questions with answers across all seniority levels — from “what are the 6 JSON data types” to “how do you prevent prototype pollution from untrusted JSON” and “design a versioning strategy for a JSON REST API.”

Junior-Level JSON Questions (Basics)

Junior JSON questions focus on the specification fundamentals: data types, syntax rules, and the two core APIs. Every engineer working with APIs or configuration files needs these answers without hesitation.

Q1. What is JSON?

JSON (JavaScript Object Notation) is a lightweight, text-based data interchange format defined in RFC 8259 and ECMA-404. It is language-independent but uses conventions familiar from JavaScript object literals. JSON is the dominant format for REST APIs, configuration files, and data storage — used by over 95% of public APIs. It is human-readable, easy to parse, and supported natively in every major programming language.

Q2. What are the 6 JSON data types?

// The 6 JSON data types with examples
{
  "string":  "hello world",          // Unicode text in double quotes
  "number":  42.5,                   // integer or float — no NaN, Infinity
  "boolean": true,                   // true or false (lowercase only)
  "null":    null,                   // explicit absence of value
  "array":   [1, "two", false],      // ordered list, mixed types allowed
  "object":  { "key": "value" }      // unordered key-value pairs, keys are strings
}

// NOT valid JSON values (JavaScript-only)
undefined        // silently omitted by JSON.stringify()
BigInt(9007199254740993n)  // throws TypeError in JSON.stringify()
NaN              // serialized as null by JSON.stringify()
Infinity         // serialized as null by JSON.stringify()
function() {}    // silently omitted by JSON.stringify()

Q3. What is the difference between JSON and a JavaScript object?

// JavaScript object literal — NOT JSON
const jsObj = {
  name: 'Alice',          // unquoted key, single-quoted value — valid JS, invalid JSON
  age: undefined,         // undefined value — valid JS, invalid JSON
  greet() { return 'hi' },  // function — valid JS, invalid JSON
  created: new Date(),    // Date object — valid JS, not a JSON type
  // comments are fine in JS source code, not in JSON
}

// Valid JSON string (what gets sent over the wire)
const jsonStr = '{"name":"Alice","age":30}'  // double quotes required for both

// Convert between them
const parsed = JSON.parse(jsonStr)            // JSON string → JS object
const serialized = JSON.stringify(jsObj)      // JS object → JSON string
// serialized === '{"name":"Alice"}' — undefined, function, Date.toISOString() handled

Q4. Why doesn't JSON support comments?

Douglas Crockford removed comments from JSON intentionally. He observed that developers were using comments to embed parsing directives (like {"// @lint-ignore"}), which would have made JSON no longer purely a data format. JSON is a data interchange format, not a configuration language — its value comes from being unambiguously machine-parseable. If you need comments in config files, use YAML, TOML, or JSON5. In practice, many editors (VS Code) support // comments in .jsonc files, but those files are not valid JSON and must be stripped before JSON.parse().

Q5. How do you validate JSON syntax?

// Option 1: JSON.parse() with try/catch — most common
function isValidJSON(str: string): boolean {
  try {
    JSON.parse(str)
    return true
  } catch {
    return false
  }
}

// Option 2: parse and catch SyntaxError with message
function parseJSON(str: string) {
  try {
    return { data: JSON.parse(str), error: null }
  } catch (e) {
    return { data: null, error: (e as SyntaxError).message }
  }
}

// Common JSON syntax errors:
JSON.parse('{"key": undefined}')    // SyntaxError — undefined is not a JSON value
JSON.parse("{'key': 'value'}")      // SyntaxError — single quotes not allowed
JSON.parse('{"key": "value",}')     // SyntaxError — trailing comma
JSON.parse('{"key": Infinity}')     // SyntaxError — Infinity is not valid
JSON.parse('// comment
{}')        // SyntaxError — comments not allowed

Q6. What is JSON.parse() vs JSON.stringify()?

// JSON.parse(text, reviver?) — string → JavaScript value
const obj = JSON.parse('{"score":42,"created":"2026-05-20"}')
// obj.score === 42 (number), obj.created === "2026-05-20" (string, NOT Date)

// With reviver: transform values during parsing
const withDates = JSON.parse('{"created":"2026-05-20T00:00:00.000Z"}', (key, val) =>
  key === 'created' ? new Date(val) : val
)
// withDates.created instanceof Date === true

// JSON.stringify(value, replacer?, space?) — JavaScript value → string
const str = JSON.stringify({ name: 'Alice', age: 30 })
// str === '{"name":"Alice","age":30}'

// With space: pretty-print
const pretty = JSON.stringify({ name: 'Alice' }, null, 2)
// pretty === '{
  "name": "Alice"
}'

// With replacer array: include only listed keys
const filtered = JSON.stringify({ a: 1, b: 2, c: 3 }, ['a', 'c'])
// filtered === '{"a":1,"c":3}'

// Edge cases
JSON.stringify(undefined)          // → undefined (not a string)
JSON.stringify({ x: undefined })   // → '{}' — key silently omitted
JSON.stringify([1, undefined, 3])  // → '[1,null,3]' — undefined → null in arrays
JSON.stringify(NaN)                // → 'null'
JSON.stringify(new Date())         // → '"2026-05-20T00:00:00.000Z"' (calls toISOString)

Mid-Level JSON Questions (Parsing and Serialization)

Mid-level questions probe practical serialization challenges: circular references, large file parsing, custom replacers, and NDJSON streaming. These topics appear in 40-60% of mid-level backend interviews.

Q7. How do you handle circular references in JSON?

// Problem: circular reference throws TypeError
const a: { name: string; self?: typeof a } = { name: 'Alice' }
a.self = a
// JSON.stringify(a) → TypeError: Converting circular structure to JSON

// Solution 1: WeakSet replacer — drops circular references
function getCircularReplacer() {
  const seen = new WeakSet()
  return (_key: string, value: unknown) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]'
      seen.add(value)
    }
    return value
  }
}
JSON.stringify(a, getCircularReplacer())
// → '{"name":"Alice","self":"[Circular]"}'

// Solution 2: flatted library — preserves structure, round-trippable
import { stringify, parse } from 'flatted'
const str = stringify(a)     // replaces circulars with index references
const restored = parse(str)  // restores the circular structure

// Solution 3: structuredClone — for cloning (not serialization)
// structuredClone() handles circulars natively but returns an object, not a string

Q8. How do you serialize a Date object?

// JSON.stringify() calls toISOString() automatically on Date objects
const now = new Date('2026-05-20T10:00:00.000Z')
JSON.stringify({ created: now })
// → '{"created":"2026-05-20T10:00:00.000Z"}'

// JSON.parse() returns a string — must manually convert back
const parsed = JSON.parse('{"created":"2026-05-20T10:00:00.000Z"}')
parsed.created instanceof Date  // false — it's a string

// Reviver to auto-convert ISO date strings
const ISO_DATE_RE = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/
const obj = JSON.parse('{"created":"2026-05-20T10:00:00.000Z"}', (key, val) =>
  typeof val === 'string' && ISO_DATE_RE.test(val) ? new Date(val) : val
)
obj.created instanceof Date  // true

// Alternative: Unix timestamp (milliseconds) — compact, unambiguous
JSON.stringify({ created: Date.now() })  // → '{"created":1716192000000}'
new Date(1716192000000)                  // restore on the other end

// Zod — automatic date coercion
import { z } from 'zod'
const schema = z.object({ created: z.coerce.date() })
schema.parse({ created: '2026-05-20T10:00:00.000Z' }).created instanceof Date  // true

Q9. What is the JSON.stringify() replacer parameter?

// Replacer as array — whitelist: include only listed keys
const user = { id: 1, name: 'Alice', password: 'secret', email: 'a@b.com' }
JSON.stringify(user, ['id', 'name', 'email'])
// → '{"id":1,"name":"Alice","email":"a@b.com"}' — password omitted

// Replacer as function — transform each value
JSON.stringify(user, (key, value) => {
  if (key === 'password') return undefined  // omit sensitive fields
  if (typeof value === 'number') return value * 100  // transform numbers
  return value
})
// → '{"id":100,"name":"Alice","email":"a@b.com"}'

// Replacer function receives key='' for the root value first
JSON.stringify({ a: 1 }, (key, value) => {
  if (key === '') return value   // root: must return value, not undefined
  return value
})

// Use case: serialize a Map (not serializable by default)
const map = new Map([['a', 1], ['b', 2]])
JSON.stringify({ map }, (key, value) =>
  value instanceof Map ? Object.fromEntries(value) : value
)
// → '{"map":{"a":1,"b":2}}'

Q10. How do you parse large JSON files without blocking?

// Problem: JSON.parse() is synchronous — blocks event loop
// A 100 MB JSON file can block Node.js for 500+ ms

// Solution 1: JSONStream — streaming parser for large arrays
import JSONStream from 'JSONStream'
import { createReadStream } from 'fs'

createReadStream('large.json')
  .pipe(JSONStream.parse('rows.*'))   // emit each item in the "rows" array
  .on('data', (row) => process(row))
  .on('end', () => console.log('done'))

// Solution 2: oboe.js — streaming with JSONPath selectors
import oboe from 'oboe'

oboe(fs.createReadStream('large.json'))
  .node('rows.*', (row) => { process(row); return oboe.drop })
  .done(() => console.log('done'))

// Solution 3: Worker threads — offload JSON.parse() to avoid blocking main thread
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads'

if (isMainThread) {
  const worker = new Worker(__filename, { workerData: { jsonStr: largeJsonString } })
  worker.on('message', (result) => console.log('parsed:', result))
} else {
  const parsed = JSON.parse(workerData.jsonStr)
  parentPort?.postMessage(parsed)
}

// Rule of thumb:
// < 1 MB  → JSON.parse() is fine (< 5 ms)
// 1–5 MB  → JSON.parse() acceptable but monitor (5–50 ms)
// > 5 MB  → use streaming parser or Worker thread

Q11. What is NDJSON and when do you use it?

// NDJSON: one JSON value per line, separated by 

{"id":1,"event":"click","ts":"2026-05-20T10:00:01Z"}
{"id":2,"event":"scroll","ts":"2026-05-20T10:00:02Z"}
{"id":3,"event":"submit","ts":"2026-05-20T10:00:03Z"}

// Parse NDJSON — line by line
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

const rl = createInterface({ input: createReadStream('events.ndjson') })
rl.on('line', (line) => {
  if (line.trim()) {
    const event = JSON.parse(line)
    processEvent(event)
  }
})

// Produce NDJSON — one JSON.stringify per line
const events = [{ id: 1, type: 'click' }, { id: 2, type: 'scroll' }]
const ndjson = events.map(e => JSON.stringify(e)).join('
')

// Use cases:
// 1. Log files — each log entry is one NDJSON line
// 2. OpenAI / Anthropic streaming responses — each SSE data is NDJSON
// 3. Elasticsearch bulk API — alternating index + document lines
// 4. ETL pipelines — process 10M records without loading all into memory

// vs standard JSON array:
// JSON array:  must load entire file to begin parsing
// NDJSON:      process line-by-line, O(1) memory per record

Q12. How do you deeply clone an object using JSON?

// JSON round-trip clone — simple but lossy
const original = { a: 1, b: { c: 2 }, arr: [3, 4] }
const clone = JSON.parse(JSON.stringify(original))
clone.b.c = 99
original.b.c  // still 2 — deep clone, not a reference

// Limitations of JSON clone:
const lossy = {
  fn: () => 42,           // function → omitted
  undef: undefined,        // undefined → omitted
  date: new Date(),        // Date → string (loses Date prototype)
  nan: NaN,                // NaN → null
  inf: Infinity,           // Infinity → null
  map: new Map([['k',1]]), // Map → {} (empty object)
  set: new Set([1,2,3]),   // Set → {} (empty object)
}
JSON.parse(JSON.stringify(lossy))
// → { date: "2026-05-20T...", nan: null, inf: null, map: {}, set: {} }

// Better alternative: structuredClone() (Node 17+, all modern browsers)
const cloned = structuredClone(original)   // handles Map, Set, Date, circular refs
// Does NOT handle functions or class instances (prototype chain not preserved)

// Performance: JSON clone is ~3x slower than structuredClone for large objects
// Use structuredClone when available; JSON clone only for simple JSON-safe objects

Senior-Level JSON Questions (Schema and Security)

Senior questions require deep knowledge of the JSON attack surface and validation ecosystems. Prototype pollution, JSON Schema internals, and HMAC verification appear in 40-55% of senior-level interviews at security-conscious companies.

Q13. What is prototype pollution and how do you prevent it?

// Attack: malicious JSON payload pollutes Object.prototype
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}')

// Vulnerable merge — spreads __proto__ into Object.prototype
function vulnerableMerge(target: object, source: object) {
  for (const [k, v] of Object.entries(source)) {
    ;(target as Record<string, unknown>)[k] = v  // sets Object.prototype.isAdmin
  }
}
vulnerableMerge({}, payload)

const user = {}
console.log((user as { isAdmin?: boolean }).isAdmin)  // true — EVERY object is now admin!

// Prevention 1: Object.create(null) — no prototype to pollute
function safeMerge(source: Record<string, unknown>) {
  const target = Object.create(null) as Record<string, unknown>
  for (const [k, v] of Object.entries(source)) {
    if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue
    target[k] = v
  }
  return target
}

// Prevention 2: Zod schema — rejects unknown/extra keys
import { z } from 'zod'
const schema = z.object({ name: z.string(), role: z.enum(['user', 'admin']) }).strict()
schema.parse(payload)  // throws ZodError — __proto__ not in schema

// Prevention 3: Ajv with additionalProperties: false
import Ajv from 'ajv'
const ajv = new Ajv()
const validate = ajv.compile({
  type: 'object',
  properties: { name: { type: 'string' } },
  additionalProperties: false,   // rejects __proto__ and any unknown key
})
validate(payload)  // returns false

// Prevention 4: secure-json-parse library
import parseJSON from 'secure-json-parse'
parseJSON('{"__proto__": {"isAdmin": true}}')  // sanitizes __proto__ key

Q14. How do JSON schema validators like Ajv work?

import Ajv from 'ajv'
import addFormats from 'ajv-formats'

const ajv = new Ajv({ allErrors: true })  // collect all errors, not just the first
addFormats(ajv)                           // adds "email", "date-time", "uri" formats

// Define a JSON Schema
const schema = {
  type: 'object',
  properties: {
    id:      { type: 'string', format: 'uuid' },
    email:   { type: 'string', format: 'email' },
    age:     { type: 'integer', minimum: 0, maximum: 150 },
    tags:    { type: 'array', items: { type: 'string' }, maxItems: 10 },
    address: {
      type: 'object',
      properties: {
        country: { type: 'string', minLength: 2, maxLength: 2 },
      },
      required: ['country'],
      additionalProperties: false,
    },
  },
  required: ['id', 'email'],
  additionalProperties: false,  // reject unknown keys (prevents prototype pollution)
}

// Compile once (expensive) — reuse validate function (fast)
const validate = ajv.compile(schema)

// Validate — synchronous, returns boolean
const valid = validate({ id: '550e8400-e29b-41d4-a716-446655440000', email: 'a@b.com', age: 30 })
if (!valid) console.error(validate.errors)

// How Ajv achieves 1M validations/sec:
// ajv.compile() generates a JavaScript function as a string and evals it
// The generated function is a series of if/else checks — no schema interpretation at runtime
// This is 10-100x faster than interpreting the schema on every call

Q15. What is the difference between JSON Schema draft-07 and draft-2020-12?

JSON Schema draft-07 (2018) is the most widely deployed version — supported by Ajv, jsonschema (Python), and most validators. Key draft-07 keywords: $ref (local and remote references), if/then/else (conditional validation), readOnly/writeOnly (OpenAPI-friendly hints), and contentEncoding/contentMediaType. Draft-2020-12 introduced breaking changes: $ref can now appear alongside other keywords (previously prohibited); $defs replaces definitions; prefixItems replaces items for tuple validation; unevaluatedProperties and unevaluatedItems close schema gaps that additionalProperties could not cover; and vocabulary declarations allow custom keywords. Use draft-07 for maximum library compatibility; use draft-2020-12 for new projects with Ajv 8+ where you need unevaluatedProperties.

Q16. How do you prevent JSON injection into SQL queries?

// Problem: extracting JSON field values into SQL without parameterization
// Vulnerable — JSON value interpolated directly into query string
const name = JSON.parse(req.body).name  // attacker controls this
const query = `SELECT * FROM users WHERE name = '${name}'`
// If name = "Alice' OR '1'='1", query returns all rows

// Solution 1: Parameterized queries — ALWAYS use for SQL
// PostgreSQL (pg / node-postgres)
const { rows } = await pool.query(
  'SELECT * FROM users WHERE name = $1',
  [name]   // driver handles escaping
)

// Solution 2: Validate JSON with Zod before using in SQL
import { z } from 'zod'
const schema = z.object({
  name: z.string().max(100).regex(/^[a-zA-Zs]+$/),  // allowlist characters
})
const { name: safeName } = schema.parse(JSON.parse(req.body))
// Now safeName is guaranteed to match the regex

// Solution 3: Stored JSON columns — use database JSON operators safely
// PostgreSQL JSONB column — use parameterized JSON path operators
await pool.query(
  "SELECT * FROM events WHERE data ->> 'userId' = $1",
  [userId]  // $1 is parameterized — safe
)

// NOT safe — never interpolate JSON field values directly:
// WHERE data ->> 'field' = '${userInput}'  ← SQL injection risk

Q17. What are the HMAC signature verification steps for webhook JSON?

import { createHmac, timingSafeEqual } from 'crypto'

// Webhook signature verification (e.g., GitHub, Stripe, Svix pattern)
function verifyWebhookSignature(
  rawBody: Buffer,       // IMPORTANT: use raw body bytes, NOT parsed JSON
  signatureHeader: string,  // e.g., "sha256=abc123..."
  secret: string
): boolean {
  // Step 1: compute HMAC-SHA256 of the raw request body bytes
  const hmac = createHmac('sha256', secret)
  hmac.update(rawBody)
  const expectedSignature = 'sha256=' + hmac.digest('hex')

  // Step 2: compare with constant-time comparison to prevent timing attacks
  const sigBuffer = Buffer.from(signatureHeader, 'utf8')
  const expectedBuffer = Buffer.from(expectedSignature, 'utf8')

  if (sigBuffer.length !== expectedBuffer.length) return false
  return timingSafeEqual(sigBuffer, expectedBuffer)
}

// Express middleware — must use express.raw() NOT express.json()
// Parsing JSON first changes the body bytes — signature will not match
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-hub-signature-256'] as string
  if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature')
  }
  const event = JSON.parse(req.body.toString())
  // process event...
})

// Why raw body? JSON.parse(JSON.stringify(obj)) may reorder keys or
// change whitespace — the HMAC is over the exact bytes the sender signed

API Design JSON Questions

API design questions assess whether an engineer can build JSON APIs that are consistent, evolvable, and standards-compliant. These appear in 50-70% of staff-level system design rounds.

Q18. What makes a JSON error response RFC 7807 compliant?

// RFC 7807 Problem Details — standard JSON error envelope
// Content-Type: application/problem+json
{
  "type": "https://jsonic.io/errors/validation-failed",  // URI identifying error type
  "title": "Validation Failed",                           // human-readable summary
  "status": 422,                                          // HTTP status code (number)
  "detail": "The 'email' field must be a valid email address.",  // specific explanation
  "instance": "/api/users/register",                      // URI of this specific error
  // Extensions — add any additional fields
  "errors": [
    { "field": "email", "message": "Invalid email format" },
    { "field": "age",   "message": "Must be 18 or older" }
  ],
  "traceId": "abc123xyz"
}

// Minimal RFC 7807 response — only type and title are required
{
  "type": "about:blank",      // use "about:blank" when no type URI is defined
  "title": "Not Found",
  "status": 404
}

// Express utility — send RFC 7807 errors
function problemJson(res: Response, status: number, title: string, detail?: string) {
  res.status(status)
     .type('application/problem+json')
     .json({ type: 'about:blank', title, status, detail })
}

Q19. What are breaking vs non-breaking JSON API changes?

Non-breaking (additive) changes — safe to deploy without client updates: adding new optional fields to response objects; adding new optional request body fields; adding new enum values (unless clients use exhaustive switches); adding new endpoints; relaxing validation constraints (e.g., increasing max length). Breaking changes — require client updates or versioning: removing or renaming existing response fields; changing a field's data type (e.g., id from number to string); making optional fields required; changing the meaning of an existing field; removing enum values; changing URL structure or HTTP method. Principle: never remove, always add. Use semantic versioning for JSON APIs — bump the major version for breaking changes.

Q20. How do you version a JSON REST API?

// Strategy 1: URL path versioning (most common, easiest to test in browser)
GET /api/v1/users/123
GET /api/v2/users/123   // v2 may return different JSON shape

// Strategy 2: Accept header versioning (RESTful purists prefer this)
GET /api/users/123
Accept: application/vnd.jsonic.v2+json

// Strategy 3: Query parameter (simplest but pollutes URLs)
GET /api/users/123?version=2

// Strategy 4: Header versioning
GET /api/users/123
API-Version: 2026-05-20   // date-based (Azure, Stripe pattern)

// Versioning the JSON schema — keep backward-compatible fields
// v1 response
{ "id": 123, "name": "Alice" }

// v2 response — id changed from number to string (breaking change)
// Deploy v2 as new path; keep v1 running until clients migrate
{ "id": "123", "name": "Alice", "displayName": "Alice Smith" }

// Field-level versioning — embed version in the response
{
  "_version": "2",
  "id": "123",
  "name": "Alice"
}

// Sunset header — announce deprecation to API clients
// HTTP/1.1 200 OK
// Sunset: Sat, 01 Jan 2027 00:00:00 GMT
// Deprecation: true
// Link: <https://jsonic.io/api/v2/users>; rel="successor-version"

Q21. What is the JSON:API specification?

JSON:API (jsonapi.org) is a specification for building consistent REST APIs that use JSON. It defines a standard response envelope with five top-level fields: data (primary resource or resource array), errors (array of error objects — mutually exclusive with data), meta (non-standard metadata like pagination counts), links (pagination and self-referential URLs), and included (related resources included to avoid N+1 fetches). Each resource object has id, type, attributes, and relationships. JSON:API is used by Ember.js (natively), Drupal (JSON:API module), and many Rails APIs (jsonapi-resources gem). Its biggest benefits are standardized pagination, sparse fieldsets (?fields[articles]=title,body), and compound documents (the included array).

Q22. How do you paginate JSON API responses?

// Pattern 1: Offset pagination — simple, good for random access
{
  "data": [{ "id": "1" }, { "id": "2" }],
  "meta": {
    "total": 1000,
    "page": 3,
    "perPage": 20,
    "totalPages": 50
  },
  "links": {
    "self":  "/api/articles?page=3&per_page=20",
    "first": "/api/articles?page=1&per_page=20",
    "prev":  "/api/articles?page=2&per_page=20",
    "next":  "/api/articles?page=4&per_page=20",
    "last":  "/api/articles?page=50&per_page=20"
  }
}

// Pattern 2: Cursor pagination — stable for real-time data, infinite scroll
{
  "data": [{ "id": "abc" }, { "id": "def" }],
  "meta": { "hasNextPage": true, "hasPrevPage": false },
  "links": {
    "next": "/api/events?after=def&limit=20",
    "prev": null
  }
}

// Pattern 3: Keyset pagination — most performant for large datasets
// Uses indexed column (e.g., created_at + id) as cursor
// SELECT * FROM articles WHERE (created_at, id) < ($cursor_ts, $cursor_id)
// ORDER BY created_at DESC, id DESC LIMIT 20

// Trade-offs:
// Offset:  simple, supports random page access, but drifts on concurrent inserts
// Cursor:  stable for streams, no random access, requires stable sort column
// Keyset:  O(log n) query time via index, no skip overhead, most scalable

Q23. How do you design JSON for a real-time WebSocket API?

// WebSocket JSON message envelope — type-discriminated union
// Client → Server: action messages
{ "type": "subscribe",  "channel": "prices",  "symbol": "BTC" }
{ "type": "unsubscribe","channel": "prices",  "symbol": "BTC" }
{ "type": "ping",       "id": "req_001" }

// Server → Client: event messages
{ "type": "subscribed", "channel": "prices", "symbol": "BTC" }
{
  "type": "update",
  "channel": "prices",
  "symbol": "BTC",
  "price": 67842.50,
  "ts": 1716192000123   // Unix ms — more compact than ISO string
}
{ "type": "pong", "id": "req_001" }
{
  "type": "error",
  "code": "RATE_LIMITED",
  "message": "Too many subscriptions",
  "retryAfter": 5000
}

// Design principles for WebSocket JSON:
// 1. Always include a "type" discriminant — enables switch/case routing
// 2. Use integer timestamps (Unix ms) — shorter than ISO strings
// 3. Include sequence numbers for at-least-once delivery guarantees
// 4. Keep messages small — large JSON payloads increase latency
// 5. Consider binary formats (MessagePack) for high-frequency updates (>100/sec)
// 6. Include a correlation ID on requests so responses can be matched

Performance JSON Questions

Performance questions probe the throughput boundaries of JSON parsing and when to reach for alternatives. These appear most often in staff-level and principal-level interviews at high-traffic companies.

Q24. What is the throughput difference between JSON.parse() and simdjson?

JSON.parse() in V8 (Node.js) parses approximately 200-400 MB/s for typical JSON. simdjson uses SIMD CPU instructions (AVX2/SSE4) to parse JSON at 2-3 GB/s — 5-10x faster than software parsers. The Node.js bindings (simdjson-node) achieve ~1 GB/s in practice due to Node.js/C++ boundary overhead. simdjson is beneficial for: high-frequency log parsing, API gateways that parse thousands of JSON payloads per second, and analytics pipelines. For most web applications parsing individual API responses, the difference is imperceptible — a 10 KB response parsed at 200 MB/s takes 0.05 ms; at 2 GB/s it takes 0.005 ms. The 0.045 ms difference is irrelevant compared to network latency. Use simdjson only when profiling shows JSON parsing is the actual bottleneck.

Q25. When should you use MessagePack instead of JSON?

// MessagePack: binary serialization format — compatible with JSON types
// Advantages over JSON:
// 1. Smaller payload: 20-50% smaller than JSON (no key repetition, binary encoding)
// 2. Faster parsing: no UTF-8 decoding or string parsing overhead
// 3. More types: supports binary (Buffer/Uint8Array), timestamps natively

import { pack, unpack } from 'msgpackr'

const data = { id: 1, name: 'Alice', scores: [98, 87, 92] }

const jsonBytes = Buffer.byteLength(JSON.stringify(data))  // ~43 bytes
const msgpackBytes = pack(data).byteLength                  // ~28 bytes — 35% smaller

const encoded = pack(data)
const decoded = unpack(encoded)  // equivalent to JSON.parse, but faster

// Use MessagePack when:
// - High-frequency binary WebSocket messages (game state, financial data, IoT)
// - Internal microservice communication where human-readability is not needed
// - Mobile APIs where bandwidth is expensive (saves 20-50% on payload size)
// - Large arrays of numbers (floats encode as 4/8 bytes, not 15+ character strings)

// Keep JSON when:
// - Browser DevTools / curl debugging is important
// - Public APIs (humans read JSON, not MessagePack)
// - CDN caching (CDNs cache text; binary may need extra config)
// - Log files (grep and jq work on JSON, not binary)

// Rule: prefer JSON until profiling shows it is the bottleneck

Q26. How do you cache pre-serialized JSON strings?

// Problem: JSON.stringify() is CPU-intensive for large, frequently-served objects
// Solution: pre-serialize and cache the string, serve it directly

// Pattern 1: In-memory cache of serialized JSON
const cache = new Map<string, { json: string; expires: number }>()

async function getCachedJSON(key: string, getData: () => Promise<unknown>, ttlMs: number) {
  const cached = cache.get(key)
  if (cached && cached.expires > Date.now()) {
    return cached.json   // return pre-serialized string, no stringify overhead
  }
  const data = await getData()
  const json = JSON.stringify(data)  // serialize once
  cache.set(key, { json, expires: Date.now() + ttlMs })
  return json
}

// Pattern 2: Express — send pre-serialized string directly
app.get('/api/config', async (req, res) => {
  const json = await getCachedJSON('config', fetchConfig, 60_000)
  res.setHeader('Content-Type', 'application/json')
  res.send(json)   // send string directly — avoids res.json() re-serialization
})

// Pattern 3: Redis — cache serialized JSON in Redis, serve from cache
import { createClient } from 'redis'
const redis = createClient()

async function getFromRedis(key: string) {
  const cached = await redis.get(key)       // returns string or null
  if (cached) return cached                 // already serialized — send directly
  const data = await fetchExpensiveData()
  const json = JSON.stringify(data)
  await redis.setEx(key, 60, json)          // cache for 60 seconds
  return json
}

// Benefit: one JSON.stringify per cache TTL window instead of per request
// For 1000 req/s with 60s TTL: 1 serialization per 60k requests

Q27. What is the impact of deeply nested JSON on parsing performance?

Deeply nested JSON has two performance costs: parsing (each nesting level requires a recursive or stack-based descent into the parser — depth 100+ is rarely a problem for parsers, but pathological nesting like depth 10,000 can cause stack overflow in recursive parsers) and property access (a.b.c.d.e.f requires 6 property lookups; V8 optimizes common access patterns but deeply nested structures miss the "fast path"). The practical rule: avoid nesting deeper than 4-5 levels in API responses because (a) it is hard to consume without destructuring hell, (b) schema validation is slower on deeply nested structures, and (c) partial updates (PATCH) become complex. Flatten related data using includes or embedded IDs rather than deep nesting. For example, { "author": { "id": "1" } } is better than { "author": { "profile": { "settings": { "name": "Alice" } } } }.

Q28. How do you profile JSON bottlenecks in Node.js?

// Step 1: Identify if JSON is the bottleneck — V8 CPU profiler
node --prof server.js
# Run load test, then:
node --prof-process isolate-*.log | head -50
# Look for JSON.stringify / JSON.parse in the "Bottom up" section

// Step 2: Benchmark with clinic.js
npm install -g clinic
clinic flame -- node server.js
# Opens a flame graph — wide JSON bars indicate serialization bottleneck

// Step 3: Micro-benchmark with console.time
const obj = { /* large object */ }

console.time('stringify')
for (let i = 0; i < 10_000; i++) JSON.stringify(obj)
console.timeEnd('stringify')   // "stringify: 450ms" → ~45 μs per call

// Step 4: Check if parsing or serialization is the bottleneck
// Often serialization (stringify) is slower than parsing (parse)
// because stringify must traverse the entire object graph

// Common fixes:
// 1. Cache JSON.stringify() output (see Q26)
// 2. Remove unnecessary fields with a replacer array (smaller output = faster)
// 3. Use fast-json-stringify for known-shape objects (generates a serialize function)
import fastJsonStringify from 'fast-json-stringify'

const stringify = fastJsonStringify({
  type: 'object',
  properties: {
    id:    { type: 'integer' },
    name:  { type: 'string' },
    email: { type: 'string' },
  },
})
stringify({ id: 1, name: 'Alice', email: 'a@b.com' })
// 2-5x faster than JSON.stringify() for objects with known shape

Language-Specific JSON Questions

Language-specific questions test knowledge of ecosystem idioms and common gotchas. These typically appear in the last 15 minutes of a technical interview to assess depth in the candidate's primary language.

Q29. How does Python's json.loads() differ from orjson?

# Python standard library json module
import json

data = json.loads('{"name":"Alice","created":"2026-05-20"}')
# data["created"] is a string — no automatic date parsing
# json.dumps() returns str

# orjson — Rust-backed, 5-10x faster, returns bytes
import orjson

data = orjson.loads(b'{"name":"Alice","scores":[1,2,3]}')
# returns dict, same as json.loads()

encoded = orjson.dumps({"name": "Alice", "created": datetime(2026, 5, 20)})
# returns bytes (not str!) — auto-serializes datetime to ISO 8601
# b'{"name":"Alice","created":"2026-05-20T00:00:00"}'

# Key differences:
# - orjson.dumps() returns bytes; json.dumps() returns str
# - orjson serializes datetime, numpy arrays, UUID natively
# - orjson raises orjson.JSONDecodeError (not json.JSONDecodeError)
# - orjson rejects NaN/Infinity by default; json allows them (non-standard)
# - orjson is 5-10x faster for large payloads (Rust SIMD parser)

# When to use which:
# json: scripts, no dependency constraints, small payloads
# orjson: APIs, data pipelines, anywhere throughput matters

Q30. What is serde in Rust?

// serde is Rust's serialization/deserialization framework
// serde_json is the JSON implementation — zero-copy, compiles to fast native code

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

// Derive macros — generate serialize/deserialize code at compile time
#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u64,
    name: String,
    #[serde(rename = "emailAddress")]   // JSON key differs from Rust field name
    email: String,
    #[serde(skip_serializing_if = "Option::is_none")]  // omit null fields
    age: Option<u32>,
}

// Deserialize JSON → Rust struct (type-safe at compile time)
let json_str = r#"{"id":1,"name":"Alice","emailAddress":"a@b.com"}"#;
let user: User = serde_json::from_str(json_str).expect("Invalid JSON");

// Serialize Rust struct → JSON string
let json_output = serde_json::to_string(&user).unwrap();
// → '{"id":1,"name":"Alice","emailAddress":"a@b.com"}'

// Untyped JSON — serde_json::Value (like JavaScript's any)
let v: Value = serde_json::from_str(r#"{"x":1}"#).unwrap();
let x = v["x"].as_i64().unwrap();   // 1

// json! macro — construct JSON values inline
let payload = json!({ "name": "Alice", "scores": [98, 87] });

Q31. What are the gotchas of Kotlin data classes with Gson?

// Gotcha 1: Gson ignores Kotlin non-null types — creates null values
data class User(val name: String, val age: Int)

val gson = Gson()
val user = gson.fromJson<User>("{}", User::class.java)
// user.name === null — Kotlin thinks it's non-null, but Gson bypasses the check
// Fix: use kotlinx.serialization or Moshi (both understand Kotlin nullability)

// Gotcha 2: Default parameter values are ignored by Gson
data class Config(val timeout: Int = 30, val retries: Int = 3)
gson.fromJson<Config>("{}", Config::class.java)
// Config(timeout=0, retries=0) — defaults ignored, primitives get zero values

// Fix: use Moshi with Kotlin codegen
// @JsonClass(generateAdapter = true)
// data class Config(val timeout: Int = 30, val retries: Int = 3)

// Gotcha 3: Gson uses reflection, not annotation processors
// No compile-time errors for missing fields — failures at runtime only

// Recommended: kotlinx.serialization (official Kotlin, compile-time safe)
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(val name: String, val age: Int = 0)

val json = Json { ignoreUnknownKeys = true }
val user = json.decodeFromString<User>('{"name":"Alice"}')
// user.age === 0 (default applied correctly)

Q32. How does Go handle JSON null vs zero values?

// Go's encoding/json cannot distinguish null from missing for non-pointer types
type User struct {
    Name string  `json:"name"`
    Age  int     `json:"age"`
}

// json.Unmarshal('{"name":"Alice"}', &u) → u.Age = 0 (zero value)
// json.Unmarshal('{"name":"Alice","age":null}', &u) → u.Age = 0 (also zero!)
// Impossible to tell if "age" was absent, 0, or null

// Fix: use pointers — nil means null/absent, non-nil means present
type User struct {
    Name *string  `json:"name"`
    Age  *int     `json:"age,omitempty"`  // omitempty: omit nil on marshal
}

// Now:
// json.Unmarshal('{"name":"Alice"}', &u) → u.Age = nil (absent)
// json.Unmarshal('{"name":"Alice","age":null}', &u) → u.Age = nil (null)
// json.Unmarshal('{"name":"Alice","age":0}', &u) → u.Age = &0 (zero, present)

// omitempty behavior — Go gotcha
type Response struct {
    Count int  `json:"count,omitempty"`   // omits count when it's 0
    Items []string `json:"items,omitempty"` // omits items when slice is nil or empty
}
// If count = 0 legitimately, use *int to distinguish 0 from absent

Q33. What is the Jackson ObjectMapper singleton pattern?

// Jackson (Java) — ObjectMapper is expensive to construct (parses annotations)
// CORRECT: singleton, thread-safe after configuration

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())                    // Java 8 date/time
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)  // ignore extra fields
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)  // ISO 8601 dates
            .enable(MapperFeature.DEFAULT_VIEW_INCLUSION);
    }
}

// WRONG: creating ObjectMapper per request — 10-100x slower
public String serialize(Object obj) {
    return new ObjectMapper().writeValueAsString(obj);  // DON'T DO THIS
}

// Thread safety: ObjectMapper is thread-safe once configured — reuse it
// ObjectWriter/ObjectReader (from mapper.writer() / mapper.reader()) are also thread-safe
// Jackson modules must be registered BEFORE first use — configure at startup

System Design JSON Questions

System design JSON questions test architectural thinking at the principal engineer level — how JSON interacts with distributed systems, schema evolution, and data consistency at scale.

Q34. How would you design a JSON-based configuration system for a microservices architecture?

A microservices JSON configuration system needs four properties: versioned schemas, runtime reloading, secret separation, and environment overrides. Architecture: store configs as JSON files in Git (GitOps pattern) with a JSON Schema for each service — schema validation runs in CI before merge. A config server (like HashiCorp Consul or a simple S3 bucket + CloudFront) serves configs over HTTP with a content hash ETag for change detection. Services poll the config endpoint every 30 seconds and compare ETags; on change, reload in-memory state without restarting. Never include secrets in JSON config — use a separate secrets manager (AWS Secrets Manager, HashiCorp Vault) and inject secrets as environment variables at startup. Layer configs: default.json (base) overridden by production.json overridden by environment variables — JSON merge patch (RFC 7396) handles deep merging. JSON Schema draft-2020-12 with unevaluatedProperties: false prevents unknown keys from silently passing validation.

Q35. How would you implement a distributed JSON event log?

// Distributed JSON event log — each event is an immutable NDJSON record
// Written to an append-only log (Kafka, Kinesis, or a file-based log)

// Event envelope — every event has the same wrapper
{
  "eventId":   "01HX7MZPQ3QVBW8TXCA5FYBN2T",  // ULID (sortable UUID)
  "eventType": "order.placed",                   // dot-namespaced type
  "version":   1,                                // schema version for this event type
  "aggregateId": "order-123",                    // entity this event belongs to
  "aggregateType": "Order",
  "causationId": "req-abc",                      // request that caused this event
  "correlationId": "trace-xyz",                  // distributed trace ID
  "ts": 1716192000123,                           // Unix ms — sortable, unambiguous
  "payload": {
    "customerId": "cust-456",
    "items": [{ "sku": "X001", "qty": 2, "price": 29.99 }],
    "total": 59.98
  }
}

// Key design decisions:
// 1. ULID eventId: lexicographically sortable, globally unique, no coordination needed
// 2. version field: allows payload schema to evolve — consumers check version
// 3. Immutable: events are never modified — corrections are new events (e.g., "order.corrected")
// 4. NDJSON on disk: append-only, readable with grep/jq, no parse overhead to read a line
// 5. Compaction: periodically snapshot current state + discard old events (Kafka log compaction)

// Consumer — read NDJSON stream and rebuild state
for await (const line of readLines('events.ndjson')) {
  const event = JSON.parse(line)
  if (event.eventType === 'order.placed') applyOrderPlaced(event)
  if (event.eventType === 'order.shipped') applyOrderShipped(event)
}

Q36. How do you handle schema migrations for JSON stored in a document database?

Document databases (MongoDB, DynamoDB, Firestore) store JSON without enforcing a schema — old documents may have different shapes than new documents. Three strategies: Lazy migration — when a document is read, if its schema version is old, migrate it in the application layer and optionally write the migrated version back. Fast to deploy, but the codebase must support N versions simultaneously until all documents migrate. Background migration — run a background job that reads old documents in batches, transforms the JSON, and writes the new version. Set a deadline and track progress with a version field. Schema versioning with discriminants — every document has a _schemaVersion field; the application reads the version and dispatches to the appropriate decoder. Combined with Zod discriminated unions, each version has a typed schema. Best practice: always add a _schemaVersion field from the start, default to 1, increment on breaking changes. Never remove a decoder until no documents with the old version remain.

Q37. How would you build a JSON diff and merge system?

// JSON Patch (RFC 6902) — express diffs as an array of operations
// Operations: add, remove, replace, move, copy, test

const patch = [
  { "op": "replace", "path": "/name", "value": "Alice Smith" },
  { "op": "add",     "path": "/tags/0", "value": "admin" },
  { "op": "remove",  "path": "/deprecated" },
  { "op": "test",    "path": "/version", "value": 1 }  // assert before patching
]

// Apply with fast-json-patch library
import { applyPatch, compare } from 'fast-json-patch'

const original = { name: 'Alice', version: 1 }
const patched = { name: 'Alice Smith', version: 1, tags: ['admin'] }

const diff = compare(original, patched)
// → same as patch above

applyPatch(original, diff)   // mutates original (or use deepClone option)

// JSON Merge Patch (RFC 7396) — simpler but less expressive
// Send only the changed fields; null means delete
const mergePatch = { name: 'Alice Smith', deprecated: null }
// Apply: shallowly merge — null values delete keys
function applyMergePatch(target: Record<string, unknown>, patch: Record<string, unknown>) {
  for (const [k, v] of Object.entries(patch)) {
    if (v === null) delete target[k]
    else if (typeof v === 'object' && v !== null && typeof target[k] === 'object')
      applyMergePatch(target[k] as Record<string, unknown>, v as Record<string, unknown>)
    else target[k] = v
  }
}

// Three-way merge — handle concurrent edits
// Base: original document
// Ours: our changes (JSON Patch from base → ours)
// Theirs: their changes (JSON Patch from base → theirs)
// Conflict: both patches modify the same path — requires human resolution or LWW strategy

Q38-40. Additional System Design JSON Questions

Q38: How do you design a JSON-based feature flag system? Each flag is a JSON object with key, enabled, rolloutPercentage, conditions (array of targeting rules), and variants (A/B test values). Store flags in a JSON file in Git for audit trail; serve from a CDN with aggressive caching (5-minute TTL); evaluate flags client-side using a hash of userId + flagKey modulo 100 for deterministic rollout. The evaluation logic is a JSON Schema-validated rule engine — no code changes required to change flag targeting.

Q39: How do you secure JSON in transit and at rest? In transit: always use TLS 1.3; set Content-Type: application/json and validate it server-side to prevent content-type confusion attacks; use HMAC signatures on webhooks (Q17); add Content-Security-Policy to prevent JSONP-based data exfiltration. At rest: encrypt sensitive JSON fields individually (envelope encryption — field-level encryption key wrapped by a master key) rather than encrypting the entire document, so you can query on non-sensitive fields without decrypting. Use deterministic encryption for indexed fields (allows equality queries) and random-IV encryption for non-indexed fields. JSON Web Encryption (JWE, RFC 7516) provides a standard envelope for encrypted JSON payloads.

Q40: How would you build a JSON API gateway that enforces schema contracts? At the gateway layer, validate every inbound request body against a JSON Schema registry (versioned schemas stored in a central service). Reject requests that fail validation with RFC 7807 error responses before they reach the upstream service — this prevents malformed data from propagating through the system. For responses, validate the upstream service's JSON response against the published response schema before returning to the client — this surfaces schema drift early. Use the JSON data validation approach with Ajv compiled validators (not interpreted) to keep validation overhead below 1 ms per request. Cache compiled validators keyed by schema hash — recompile only when the schema changes.

Key Terms

JSON schema
A JSON document that describes the structure, data types, and constraints of another JSON document. Defined by the JSON Schema specification (currently draft-2020-12 and draft-07, the two most widely deployed versions). A JSON schema uses keywords like type, properties, required, minimum, maxLength, and additionalProperties to constrain the valid shape of a JSON value. Validators like Ajv (JavaScript), jsonschema (Python), and go-jsonschema (Go) compile schemas to validation functions. JSON Schema is used for API contract validation, form input validation, configuration file validation, and OpenAPI spec definitions. Setting additionalProperties: false on object schemas prevents prototype pollution by rejecting unknown keys including __proto__.
prototype pollution
A JavaScript security vulnerability where a malicious JSON payload containing __proto__, constructor, or prototype as a key modifies Object.prototype or a constructor's prototype, affecting all objects in the application. The attack exploits JavaScript's prototype inheritance: when a key-value pair from a parsed JSON object is merged into a target using bracket notation (target[key] = value), a key of __proto__ writes to Object.prototype rather than the target object. Prevention requires either using Object.create(null) for merge targets, sanitizing keys before merging, or using schema validators that reject these reserved keys. The vulnerability was widely exploited through lodash before version 4.17.5.
NDJSON
Newline Delimited JSON (also called JSON Lines or JSONL) is a format where each line contains a valid, complete, self-contained JSON value separated by a newline character (\n). Unlike JSON arrays, NDJSON files can be processed line-by-line in constant memory — no need to buffer the entire document. Each line is independent: a parse error on one line does not affect others. Common use cases include log files (one event per line), streaming API responses (OpenAI, Anthropic SSE events), Elasticsearch bulk API requests (alternating metadata and document lines), and data pipeline ETL where records are processed sequentially. NDJSON files use the .ndjson or .jsonl extension; the MIME type is application/x-ndjson.
replacer function
The second parameter of JSON.stringify(value, replacer, space) — either a function or an array. When passed as a function (key, value) => transformed, it is called for every key-value pair during serialization, including the root value (called with key === ''). Returning undefined from the replacer omits the key from the output. The replacer function is called with the value after any toJSON() method has been invoked. When passed as an array, it acts as a whitelist — only keys present in the array are included in the output. Common use cases: omitting sensitive fields (passwords, tokens), serializing non-JSON types (Map, Set, Date, BigInt), handling circular references, and field renaming during serialization.
discriminated union
A JSON pattern where one field (the discriminant, typically type or kind) identifies which variant of a union type the object represents, allowing consumers to safely narrow the type without runtime errors. Example: {"type":"circle","radius":5} and {"type":"rect","width":10,"height":20} share a discriminant of type. In TypeScript, discriminated unions are first-class: switch (shape.type) { case "circle": ...; case "rect": ... } gives full type inference in each branch. In JSON Schema, the discriminator keyword (OpenAPI 3.1) or oneOf with const constraints implements this pattern. Discriminated unions are preferred over the GraphQL errors array for application-level errors, encoding expected failure modes as typed, named variants.
circular reference
A situation where an object in a graph contains a reference that, when followed, eventually leads back to the same object — forming a cycle. Example: const a = ; a.self = a creates a circular reference because a.self === a. JSON has no concept of object identity or references — every value in JSON is independent — so circular references cannot be represented in JSON. JSON.stringify() throws a TypeError: Converting circular structure to JSON when it encounters a cycle. Solutions include using a WeakSet in a replacer function to detect and drop already-seen objects, the flatted library which encodes cycles as index references, or structuredClone() for in-memory cloning (which handles circulars natively but produces an object, not a JSON string).

FAQ

What are the 6 JSON data types?

JSON has exactly six data types: string (Unicode text in double quotes — single quotes are not valid JSON), number (integer or floating-point — JSON has no separate integer type, and NaN and Infinity are not valid JSON numbers), boolean (true or false in lowercase — True and False are not valid), null (the literal value null representing absence), array (an ordered list of zero or more values enclosed in square brackets, values separated by commas), and object (an unordered collection of key-value pairs enclosed in curly braces, where all keys must be double-quoted strings). JavaScript adds undefined and BigInt as additional types that JSON does not support — JSON.stringify() drops undefined silently and throws a TypeError for BigInt.

What is the difference between JSON and a JavaScript object?

JSON is a text format (a string), while a JavaScript object is an in-memory data structure. Five key differences: (1) JSON keys must always be double-quoted strings; JavaScript object keys can be unquoted identifiers, single-quoted strings, or computed expressions. (2) JSON strings must use double quotes; JavaScript allows single quotes and template literals. (3) JSON supports only its 6 types — JavaScript objects can hold undefined, functions, Symbols, Dates, Maps, Sets, BigInts, and class instances. (4) JSON forbids trailing commas; modern JavaScript allows them in object literals and arrays. (5) JSON cannot contain comments; JavaScript source code can. When you call JSON.stringify(obj), JavaScript functions, undefined values, and Symbol keys are silently dropped; Dates become ISO 8601 strings; NaN and Infinity become null. Use JSON.parse() to convert a JSON string back to a JavaScript value.

How do you handle circular references in JSON.stringify()?

JSON.stringify() throws TypeError: Converting circular structure to JSON when it encounters a circular reference. The most common fix is a replacer function using a WeakSet to track already-seen objects: const seen = new WeakSet(); return (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) return "[Circular]"; seen.add(value); } return value; }. This silently replaces circular references with the string "[Circular]", which is not round-trippable but is useful for logging and debugging. For round-trippable serialization (where you need to restore the circular structure), use the flatted npm library — it encodes circular references as indexed back-references and provides a paired parse() function. For cloning (not serialization), use structuredClone(), which natively handles circular references.

What is prototype pollution in JSON?

Prototype pollution is a JavaScript security vulnerability where a malicious JSON payload modifies Object.prototype by using __proto__ as a key. Example: JSON.parse('{"__proto__": {"isAdmin": true}}') returns an object with a __proto__ key. When this object is merged into another object using a naive loop (target[key] = value), the assignment target["__proto__"] = {"isAdmin": true} actually writes to Object.prototype, making isAdmin === true on every object in the application. This can bypass authorization checks. Prevention: (1) use Object.create(null) as the merge target — it has no prototype to pollute; (2) skip keys equal to __proto__, constructor, or prototype during merge; (3) validate input with Ajv using additionalProperties: false before merging; (4) use the secure-json-parse library.

How do you validate JSON against a schema?

Schema validation checks that a JSON document conforms to a declared structure — types, required fields, value ranges, and allowed keys. The two primary tools are Ajv and Zod. Ajv uses JSON Schema (draft-07 or draft-2020-12): call ajv.compile(schema) once to produce an optimized validator function, then call validate(data) per request — Ajv achieves ~1 million validations/second by generating JavaScript code from the schema at compile time. Zod is TypeScript-first: define a schema with z.object({ ... }) and call schema.parse(data) — it throws a ZodError on failure and returns a typed value on success. Zod infers TypeScript types from the schema, keeping type definitions and runtime validation in sync. For API input validation, always set additionalProperties: false (Ajv) or .strict() (Zod) to reject unknown keys. See the JSON data validation guide for detailed comparisons.

What is the difference between JSON.parse() and eval() for parsing JSON?

JSON.parse() is the correct and safe method; eval() is a critical security vulnerability for parsing JSON. eval() executes any JavaScript code — passing untrusted user input to eval() enables remote code execution (RCE). For example, eval('{"x": (require("child_process").exec("rm -rf /"))}') would execute a destructive shell command. JSON.parse() uses a dedicated parser that only accepts valid JSON syntax — it cannot execute code, and it throws SyntaxError for anything that is not valid JSON. On performance: JSON.parse() is 2-10x faster than eval() for JSON data because V8 uses a native C++ JSON parser that is specifically optimized for JSON syntax. eval() was used before ES5 (2009) standardized JSON.parse() — any code still using eval() for JSON parsing should be considered a security incident.

How do you serialize dates in JSON?

JSON has no native Date type, so dates must be encoded as strings or numbers. Three conventions: (1) ISO 8601 string (recommended): "2026-05-20T10:00:00.000Z"JSON.stringify() calls toISOString() automatically on Date objects, so you get this for free. Human-readable and timezone-explicit (the trailing Z means UTC). Parse back with new Date("2026-05-20T10:00:00.000Z"). (2) Unix timestamp (integer milliseconds): 1716192000000 — compact, unambiguous, easy to compare and sort. Use Date.now() to produce and new Date(ts) to consume. (3) Date-only string: "2026-05-20" for dates without time. The key gotcha: JSON.parse() returns all values as JavaScript primitives — it does not automatically convert ISO date strings back to Date objects. Use a reviver function or Zod's z.coerce.date() to auto-convert.

What is NDJSON (Newline Delimited JSON)?

NDJSON (Newline Delimited JSON, also called JSON Lines or JSONL) is a format where each line is a complete, valid JSON value separated by a newline character. Unlike a standard JSON array, NDJSON can be processed one line at a time in constant memory — you never need to load the entire file. Each line is independent, so a parse error on line 500 does not invalidate the preceding 499 lines. NDJSON is used for: streaming API responses (OpenAI, Anthropic, and most LLM APIs send each token as an NDJSON line over SSE), log files (one structured log event per line, grepable with jq), the Elasticsearch bulk API (alternating index metadata and document lines), and large-scale ETL where millions of records must be processed without loading all into memory. The file extension is .ndjson or .jsonl; the MIME type is application/x-ndjson. Parse with: text.split('\n').filter(Boolean).map(line => JSON.parse(line)).

Further reading and primary sources