appscript.dev
Guide Intermediate

Parse and build JSON safely

Patterns for handling messy API responses in Northwind scripts.

Published Sep 9, 2025

JSON is the language most APIs speak, and Apps Script makes it easy to send and receive — JSON.parse turns a response body into an object, JSON.stringify turns an object into a request body. The trouble is that real-world JSON is rarely as tidy as the documentation promises.

A response can arrive truncated, wrapped in an HTML error page, missing the field you expected, or with a number where you wanted a string. If your script trusts the data blindly, a single malformed payload will throw an unhandled exception and halt the run. The patterns below let a script survive bad input instead of crashing on it.

Defensive parsing

JSON.parse throws a SyntaxError the moment it meets input it cannot read — an empty body, an HTML error page, a half-downloaded response. Wrap every parse of untrusted text in a try/catch so one bad payload cannot kill the run.

/**
 * Parse JSON text, returning a fallback value instead of throwing on bad input.
 * @param {string} text     The raw response body.
 * @param {*}      fallback Value to return when parsing fails.
 */
function tryParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch (e) {
    // Bad input — log it for debugging, then return the safe default.
    console.warn('JSON parse failed: ' + e.message);
    return fallback;
  }
}

// The fallback has the same shape the rest of the code expects,
// so downstream loops over `data.results` still work on a bad response.
const data = tryParse(res.getContentText(), { results: [] });

The key is choosing a fallback with the same shape as a valid response. If the code later does data.results.forEach(...), returning { results: [] } keeps that line safe — it simply iterates nothing.

Safe nested access

API responses nest deeply, and any layer can be missing. Reaching into response.user.contact.email throws TypeError the instant user or contact is absent. Optional chaining short-circuits to undefined instead, and Apps Script’s V8 runtime supports it fully.

// `?.` stops at the first missing link instead of throwing.
// `||` then supplies a default so the result is always a string.
const email = response?.user?.contact?.email || '';

// Works for arrays too — no crash if `items` is missing or empty.
const firstId = response?.items?.[0]?.id ?? null;

Use || when any falsy value (empty string, 0) should fall back to the default. Use ?? when only null or undefined should — it preserves a legitimate 0 or empty string.

Validating shape

Parsing succeeding does not mean the data is usable. The JSON might be valid but describe something other than what you expect. A small validation function, checked before you trust an object, catches that early.

/**
 * Returns true only if `obj` looks like an invoice we can process.
 * Checks both presence and type of each required field.
 */
function isInvoice(obj) {
  return obj
    && typeof obj.id === 'string'
    && typeof obj.amount === 'number';
}

// Filter a response down to records that actually pass the check.
const invoices = (data.results || []).filter(isInvoice);

This is deliberately lightweight — enough to reject obviously wrong records without building a full schema engine. For heavier validation, hand the JSON to Claude with a strict schema and ask it to coerce — see Extract structured data from messy text.

Stringify with care

Building JSON has its own traps. JSON.stringify quietly transforms or drops certain values, and the changes are easy to miss until a round trip loses data.

Value typeWhat JSON.stringify does
DateConverts to an ISO string — the Date type is lost
undefinedDropped from objects; becomes null in arrays
FunctionDropped entirely
NaN / InfinityBecomes null
BigIntThrows a TypeError

The Date case bites most often. Stringifying produces a string, so after JSON.parse you get a string back, not a Date. Always re-wrap it.

const payload = { createdAt: new Date() };
const text = JSON.stringify(payload);   // createdAt is now an ISO string

const restored = JSON.parse(text);
// `createdAt` is a string here — convert it back before using date methods.
restored.createdAt = new Date(restored.createdAt);

Common mistakes

  • Calling JSON.parse directly on a response body. Any non-JSON reply — an outage page, a rate-limit notice — throws and stops the script.
  • Assuming HTTP 200 means valid JSON. Use muteHttpExceptions: true on the fetch and check both the status code and the body before parsing.
  • Treating a parsed object as trusted. Valid JSON can still be the wrong shape; validate the fields you depend on.
  • Expecting a Date to survive a round trip. Stringify then parse gives a string — re-new Date() after every parse.
  • Using || where ?? is meant, so a real 0 or empty string is replaced by the fallback when it should have been kept.