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 type | What JSON.stringify does |
|---|---|
Date | Converts to an ISO string — the Date type is lost |
undefined | Dropped from objects; becomes null in arrays |
| Function | Dropped entirely |
NaN / Infinity | Becomes null |
BigInt | Throws 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.parsedirectly 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: trueon 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
Dateto survive a round trip. Stringify then parse gives a string — re-new Date()after every parse. - Using
||where??is meant, so a real0or empty string is replaced by the fallback when it should have been kept.