denoland/deno · error · TypeError

Value is not JSON serializable

Error message

Value is not JSON serializable

What it means

serializeJSValueToJSONString (ext/web/00_infra.js:330) is the helper behind Response.json() (ext/fetch/23_response.js:700) and other JSON-body infrastructure. It calls JSON.stringify and throws TypeError when stringify returns undefined, which happens exactly when the top-level value is undefined, a function, or a symbol. Nested values of those types are silently dropped by stringify and do not trigger this error.

Source

Thrown at ext/web/00_infra.js:330

/**
 * @param {unknown} cond
 * @param {string=} msg
 * @returns {asserts cond}
 */
function assert(cond, msg = "Assertion failed.") {
  if (!cond) {
    throw new AssertionError(msg);
  }
}

/**
 * @param {unknown} value
 * @returns {string}
 */
function serializeJSValueToJSONString(value) {
  const result = JSONStringify(value);
  if (result === undefined) {
    throw new TypeError("Value is not JSON serializable");
  }
  return result;
}

const PATHNAME_WIN_RE = new SafeRegExp(/^\/*([A-Za-z]:)(\/|$)/);
const SLASH_WIN_RE = new SafeRegExp(/\//g);
const PERCENT_RE = new SafeRegExp(/%(?![0-9A-Fa-f]{2})/g);

// Keep in sync with `fromFileUrl()` in `std/path/win32.ts`.
/**
 * @param {URL} url
 * @returns {string}
 */
function pathFromURLWin32(url) {
  let p = StringPrototypeReplace(url.pathname, PATHNAME_WIN_RE, "$1/");
  p = StringPrototypeReplace(p, SLASH_WIN_RE, "\\");
  p = StringPrototypeReplace(p, PERCENT_RE, "%25");
  let path = decodeURIComponent(p);

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Default the payload: Response.json(data ?? null) or Response.json(data ?? {})
  2. If you passed a function, call it: Response.json(toJson())
  3. Serialize a wrapper object so the top level is never undefined: Response.json({ value })

Example fix

// before
return new Response.json(data.user); // undefined when user missing

// after
return Response.json(data.user ?? null);
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof body === "undefined" || typeof body === "function" || typeof body === "symbol") {
  body = null;
}
return Response.json(body);

Type guard

function isTopLevelJsonSerializable(v: unknown): boolean {
  return v == null ||
    (typeof v !== "function" && typeof v !== "symbol");
}

Try / catch

let str: string;
try {
  str = JSON.stringify(value);
} catch {
  str = "{}"; // circular reference
}
if (str === undefined) str = "null"; // top-level undefined/function/symbol
return new Response(str, {
  headers: { "content-type": "application/json" },
});

Prevention

When it happens

Trigger: return Response.json(data.user) where data.user is undefined; Response.json(handler.serialize) with the call parentheses missing, passing the function itself; Response.json(Symbol('id')) as a top-level value.

Common situations: Optional fields from fetch or DB rows used directly as the whole body; passing a method reference instead of its result; refactors that turn a always-object payload into an optional one without a default.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/9da2a816285d49ee. Report an issue: GitHub.