Reader Stacks

Common Angular Utilities: Deep-Copying Objects and Parsing JSON Strings

A plain spread copy only copies the top level — any nested object or array inside it still points back to the exact same reference as the original, which is precisely how subtle mutation bugs creep in.

Two genuinely common data-handling needs in Angular — copying an object without it still referencing the original, and parsing a JSON string into a usable object — come up constantly enough to be worth a dedicated reference.

The problem with a shallow copy

const original = { name: 'Alex', address: { city: 'Austin' } };
const copy = { ...original };

copy.address.city = 'Dallas';
console.log(original.address.city); // "Dallas" — the original was mutated too!

The spread operator ({ ...original }) only copies the object's top-level properties — any nested object or array inside it is still the exact same reference shared between the original and the copy, which is precisely how this kind of subtle, hard-to-trace mutation bug happens.

A true deep copy using structuredClone()

const original = { name: 'Alex', address: { city: 'Austin' } };
const copy = structuredClone(original);

copy.address.city = 'Dallas';
console.log(original.address.city); // "Austin" — unaffected

structuredClone(), a native browser API, is the modern, recommended way to deep-copy a plain object or array — it correctly handles nested structures, though it does not work for values it can't structurally clone, like functions or class instances with methods.

The older JSON-based deep copy trick, for comparison

const copy = JSON.parse(JSON.stringify(original));

This was the common workaround before structuredClone() existed — it works for plain data but silently loses anything JSON can't represent (functions, undefined values, Date objects become plain strings) — structuredClone() is the more correct and more modern choice where browser support allows it.

Deep-copying an array of objects

const products = [{ name: 'Mouse', tags: ['electronics'] }, { name: 'Keyboard', tags: ['electronics'] }];
const copiedProducts = structuredClone(products);

Parsing a JSON string into an object

const jsonString = '{"name": "Alex", "age": 30}';
const parsed = JSON.parse(jsonString);

console.log(parsed.name); // "Alex"

Handling a malformed JSON string safely

function safeJsonParse(jsonString: string): T | null {
    try {
        return JSON.parse(jsonString) as T;
    } catch {
        return null;
    }
}

JSON.parse() throws an exception on invalid JSON rather than returning a fallback value — wrapping it in a try/catch (as shown) is necessary any time the input string's validity isn't already guaranteed, such as data read from local storage or an external, less-trusted source.

Converting an object back to a JSON string

const jsonString = JSON.stringify(parsed);
const prettyJsonString = JSON.stringify(parsed, null, 2); // indented, human-readable

The third argument to JSON.stringify() (2 here) adds indentation for human-readable output — genuinely useful for debugging or displaying formatted JSON in a UI, though it's unnecessary overhead for JSON meant only to be sent over the network or stored compactly.