What survives

Does obfuscation break JSON serialization?

JSON is where protected code meets the outside world, so a naming change here does not stay inside your bundle: it reaches a server, a stored record or another team's parser. Measured against the real engine, the serializer itself is untouched, and the two ways renaming does change your output are specific, predictable and worth knowing exactly.

Why this is the question that actually matters

Most questions about protection are about whether your program still runs. This one is different, because JSON leaves the program. A payload that serialises under a different key still parses, still passes type checks, and still looks like a valid message right up until something downstream reads a field that is no longer there.

That failure lands somewhere other than the browser, often days later, in a stored record or an integration that nobody was watching. So the useful thing is not a reassurance that JSON works; it is a precise account of when your keys can change and when they cannot.

Measured, the serializer and its hooks are entirely unaffected by protection. What can change is the spelling of your own property names, and only when you have explicitly asked for that.

What was measured

The sample exercised the parts of the API that carry behaviour rather than just data: key order for string and integer-like keys; a toJSON hook on an object and the built-in one on Date; toJSON on a nested value being called by the parent; the key argument that toJSON receives; a replacer function and a replacer allow-list array; a reviver that transforms values and a reviver that deletes a key by returning undefined; the order in which a reviver visits nodes, ending at the root; the omission rules under which undefined and functions vanish from objects but become null in arrays; indentation; and a deep round trip.

The file ran in Node, was protected, and ran again, with outputs compared line by line across the default ES5 target, the modern target, a renaming build with identifier replacement and global renaming, that build on the modern target, and a string-table build with MoveStrings and EncodeStrings enabled.

All five configurations produced identical output. The toJSON hook still ran and still received the key it was being serialised under, giving key=named under an object property and key=0 inside an array. The replacer still dropped its key, the allow-list array still restricted output to the listed keys, and the reviver still visited depth-first and ended at the root. Date still serialised to its ISO string. Keys spelled toString, valueOf and constructor still round tripped as ordinary data.

Why the serializer is untouched

None of that behaviour lives in your source text. JSON.stringify is a built-in that walks a value at run time, and the decisions it makes, which keys to visit, whether a hook exists, what order to emit, are made from the object in memory rather than from the way the program was written.

The data flowing into it is equally outside the rename sites. The keys of an object being serialised are read at run time, and any key you build from a string is a value rather than a name. A string-table build changes where those literals are stored in the file, not what they evaluate to, which is why that column matched as well.

So with the member transform off, which is the default, your JSON output after protection is byte-identical to your JSON output before it. That covers most builds, and it is the reason the failure mode discussed next is specific rather than general.

The first way renaming changes your JSON: the keys

Turn member renaming on with a pattern matching your own property names and those names change everywhere they appear, including in serialised output. Measured, an object whose property was matched by the pattern serialised under the replacement name instead of the original, both at the top level and when nested inside another object.

This is the option doing its job. A property name is a name, and renaming it is what was requested; the serializer simply reports what the object now contains. It is also the single most consequential thing to get right, because the result is valid JSON with the wrong shape, which travels further than an exception would.

The rule that follows is a boundary rule. Any property whose name is part of a contract with something outside your program, an API payload, a stored document, a message envelope, a saved preference, must not be matched by the pattern. Scope the pattern to internal names, keep contract names out of it, and where the split is awkward to express, use an explicit mapping or hold the data in a collection, whose keys are values rather than names.

The members documentation is the reference for this, and it makes the same point from the other direction: serialised data whose keys match your object shape breaks silently, with no build error to catch it.

The second way: renaming the hook itself

The subtler case is a pattern that matches toJSON. It is an easy pattern to write by accident, since a broad rule over method-like names will catch it.

Measured, the result is not an exception. When the pattern matched toJSON, the hook was renamed, JSON.stringify no longer found a method by that name, and it fell back to serialising the raw object. The output changed from the shaped value the hook produced to an empty object, silently, with the program continuing normally.

That is worth a specific check, because a serialisation hook is exactly the kind of code that is written once and then trusted. The same reasoning applies to any other name the platform looks for on your objects rather than one you call yourself: what matters is not that your code names it, but that the platform does.

The same measurement gives the reassuring half. With toJSON left out of the pattern, the hook still fired and behaved exactly as before, while the property names the pattern did match changed as instructed. The two effects are independent and both are under your control.

Scope, and how to check your own build

This article covers JSON.stringify and JSON.parse, their hooks, and the effect of member renaming on serialised output, measured on the default configuration and the four variants named above. Key ordering has its own article and is not re-derived here; the short version is that ordering is a property of the object, unchanged by protection.

The check for your own project is short and worth running once. Serialise a representative payload before and after protection with the configuration you actually ship, and compare the two JSON strings rather than the program's behaviour, because this is the one area where the program can keep working while the output has changed.

If you have integration tests that assert on payload shape, they already perform this check, and running them against a protected build is the cheapest guarantee available. If you do not, a single test that stringifies one representative object and compares it against a stored expected string is enough to catch both failure modes described here.

Frequently asked questions

Does JSON.stringify behave the same after obfuscation?

Yes, with the member transform off, which is the default. Measured across five configurations, key order, omission rules, indentation, replacers, revivers and deep round trips all produced identical output. The serializer works from the object in memory, not from your source text.

Do toJSON hooks still run after protection?

Yes, unless your member pattern matches the name toJSON. Measured, the hook ran, was called for nested values by the parent, and still received the key it was being serialised under. Date's built-in toJSON also still produced its ISO string.

What happens if member renaming matches toJSON?

The hook is renamed, JSON.stringify no longer finds a method by that name, and it falls back to serialising the raw object. Measured, this produced an empty object instead of the shaped value, with no exception and no warning. Keep toJSON out of the pattern.

Will member renaming change the keys in my API payloads?

Yes, for any property name the pattern matches. Measured, a matched property serialised under its replacement name, nested as well as at the top level. Any name that is part of a contract with a server, a stored record or another team's parser must be excluded from the pattern.

Do replacers and revivers still work?

Yes. Measured, a replacer function still dropped its key, an allow-list array still limited the output to the listed keys, a reviver still transformed values and deleted a key by returning undefined, and the reviver still visited depth-first before ending at the root.

What is the cheapest way to be sure my payloads are unchanged?

Serialise a representative object before and after protection with the configuration you ship, and compare the two JSON strings directly rather than testing that the program still runs. This is the one area where a program can keep working while its output has quietly changed shape.

Related reading