Quick answer: Before comparing API responses, test eight key-order boundaries: semantic equality, ordinary string insertion order, integer-like names, deletion and recreation, object merging, replacer arrays, recursive sorting, and canonical byte requirements. These checks separate harmless representation differences from real data changes and prevent brittle snapshots, cache keys, or signatures.
Two API responses can contain identical values yet fail a raw-string assertion because their object members arrived in another order. The opposite mistake is also possible: sorting everything can hide an array-order change that is meaningful. These eight executable tests define exactly where key order matters and where it does not.
Wikipedia's JSON overview notes that object-member order is not significant, while arrays are ordered. The technical contract comes from RFC 8259, which defines an object as an unordered collection and an array as an ordered sequence. JavaScript adds predictable enumeration behavior after parsing, but that runtime behavior does not change JSON's data model.
1. Do differently ordered objects compare as the same JSON value?
Parse differently ordered documents and compare their values rather than their source text. Object-member order should not create an API regression, while changed values, missing members, or reordered arrays should. Test both a harmless object reordering and a meaningful array reordering so the comparison policy cannot accidentally erase sequence semantics.
Node's deep equality assertion treats ordinary object properties by name rather than insertion position. The raw texts below differ, but the parsed objects match:
import assert from "node:assert/strict";
const first = '{"id":7,"name":"Ada","roles":["author","reviewer"]}';
const second = '{"roles":["author","reviewer"],"name":"Ada","id":7}';
assert.notEqual(first, second);
assert.deepEqual(JSON.parse(first), JSON.parse(second));
const reorderedRoles = '{"id":7,"name":"Ada","roles":["reviewer","author"]}';
assert.notDeepEqual(JSON.parse(first), JSON.parse(reorderedRoles));
This is the default test for ordinary API contracts. A client should not break because a server framework, database projection, or deployment changes member order. Keep array policy separate: a list of ranked results, workflow steps, coordinates, or events usually uses position as data.
For interactive review, JSON Compare reports structural changes between two valid documents. It does not decide whether an array is conceptually a set or whether exact serialized bytes form a signing contract. Put those rules in tests and protocol documentation.
2. Does JSON.stringify expose string-key insertion order?
Equivalent ordinary objects created with different string-key insertion orders normally stringify to different text. Test this before using JSON.stringify output as an equality check, snapshot identifier, cache key, or deduplication token. Stable traversal for one object is not the same as a canonical representation shared by independently constructed equivalent objects.
MDN's current JSON.stringify reference states that properties use the stable Object.keys ordering algorithm. The stability means repeated serialization of the same unchanged object matches. It does not alphabetize ordinary strings:
import assert from "node:assert/strict";
const left = { id: 7, name: "Ada", active: true };
const right = { active: true, name: "Ada", id: 7 };
assert.deepEqual(left, right);
assert.equal(
JSON.stringify(left),
'{"id":7,"name":"Ada","active":true}'
);
assert.equal(
JSON.stringify(right),
'{"active":true,"name":"Ada","id":7}'
);
assert.notEqual(JSON.stringify(left), JSON.stringify(right));
A raw-string comparison is suitable only when representation is intentionally part of the contract. Examples include a checked-in formatter fixture or a protocol-defined canonical output. Even then, lock down indentation, newline policy, escape behavior, and runtime or library versions alongside property order.
Do not call the second output unstable. It is deterministic for that object's property history. The problem is using construction history as a proxy for value equality across producers that never agreed on that history.
3. Do integer-like keys move ahead of ordinary string keys?
Test property names that qualify as JavaScript array indexes because they are enumerated before other strings and sorted numerically, not lexically or by insertion. Include boundary lookalikes such as 01, -0, and 4294967295. Numeric-looking identifiers can therefore produce surprising serialized order even when their values remain correct.
The current ECMAScript OrdinaryOwnPropertyKeys algorithm lists array-index keys first in ascending numeric order, then other string keys in chronological creation order. The definition excludes 2³² − 1 from array indexes.
import assert from "node:assert/strict";
const value = {};
value.beta = "B";
value["10"] = "ten";
value["2"] = "two";
value["01"] = "leading";
value["4294967295"] = "boundary";
value.alpha = "A";
assert.deepEqual(Object.keys(value), [
"2",
"10",
"beta",
"01",
"4294967295",
"alpha",
]);
assert.equal(
JSON.stringify(value),
'{"2":"two","10":"ten","beta":"B","01":"leading","4294967295":"boundary","alpha":"A"}'
);
This behavior is one reason a JSON object is a poor substitute for an ordered list. If sequence matters, represent entries as an array such as [{"id":"10","value":"ten"},{"id":"2","value":"two"}]. If fast lookup matters and display order is separate, store the lookup object plus an explicit order array.
Do not convert opaque identifiers to numbers merely to control display. Leading zeros and values beyond safe numeric ranges may carry meaning. Treat order as presentation or schema data, not as a side effect of property enumeration.
4. Does deleting and recreating a key change serialized order?
Deleting and then recreating a non-index string property gives it a new creation position, while merely assigning a new value does not. Test both operations if code builds responses through mutation. A refactor from update to delete-and-add can change snapshots or text diffs even when the final names and values are otherwise identical.
import assert from "node:assert/strict";
const updated = { id: 1, status: "queued", owner: "Ada" };
updated.status = "ready";
assert.deepEqual(Object.keys(updated), ["id", "status", "owner"]);
const recreated = { id: 1, status: "queued", owner: "Ada" };
delete recreated.status;
recreated.status = "ready";
assert.deepEqual(Object.keys(recreated), ["id", "owner", "status"]);
assert.deepEqual(updated, recreated);
assert.notEqual(JSON.stringify(updated), JSON.stringify(recreated));
The values compare equally because both objects end with id, owner, and the same status. Only their property histories differ. If a snapshot fails after this refactor, inspect parsed values before updating the snapshot. That prevents approval of a real change hidden among representational noise.
Index-like properties remain in their numeric group regardless of deletion and recreation. Add a separate fixture if your payload uses those names; do not generalize the string-key result to every possible property.
5. Does object merging preserve the intended keys and positions?
Test spread syntax or Object.assign with overlapping properties because later sources replace values without necessarily moving an existing key's position. New keys are appended according to enumeration rules. Assert the final values first, then order only when serialized presentation is contractual. Merging untrusted data also requires a separate allowlist and authorization policy.
import assert from "node:assert/strict";
const defaults = { id: 7, status: "draft", region: "IN" };
const update = { status: "published", title: "Guide" };
const merged = { ...defaults, ...update };
assert.deepEqual(merged, {
id: 7,
status: "published",
region: "IN",
title: "Guide",
});
assert.deepEqual(Object.keys(merged), ["id", "status", "region", "title"]);
The later status value overwrites the earlier one at the existing position; title is new and comes later. Reversing the source objects changes both precedence and likely output order. That can be a business bug, not merely formatting, so never normalize order before checking the merged values.
For request bodies, construct an explicit transfer object when a fixed human-readable field order helps diagnostics. Do not spread an entire database row, session object, or user input simply to achieve convenient ordering. The prototype-pollution checks cover unsafe merge boundaries beyond this ordering test.
6. Does a replacer array control order while dropping fields recursively?
An array-form JSON.stringify replacer supplies both an object-property allowlist and its traversal order. It applies across nested objects rather than only the root. Test the exact output and omitted fields before using it for presentation. A property list is not a general sorter, path selector, schema validator, or authorization control.
import assert from "node:assert/strict";
const response = {
id: 7,
profile: { id: 8, name: "Ada", privateNote: "internal" },
name: "Account",
ignored: true,
};
const text = JSON.stringify(response, ["name", "id", "profile"]);
assert.equal(
text,
'{"name":"Account","id":7,"profile":{"name":"Ada","id":8}}'
);
assert.equal(text.includes("privateNote"), false);
assert.equal(text.includes("ignored"), false);
The same name-before-id order is used inside profile, and privateNote disappears because it is not in the list. If nested object types have different schemas, map each object explicitly or use a carefully tested function replacer. The JSON.stringify replacer tests cover root calls, toJSON precedence, unsupported values, BigInt, and cycles.
Never interpret successful omission as proof of safe disclosure. A newly added sensitive field whose name is already allowed elsewhere can pass through at an unintended depth. Security requires a path-aware output contract.
7. Does recursive sorting preserve arrays and every supported value?
A local stable-output helper should recursively sort plain-object keys while preserving array positions, primitive types, and explicitly supported values. Test nested objects and a reordered array. Reject or define behavior for Dates, Maps, Sets, BigInt, cycles, class instances, and getters rather than presenting a small helper as a universal serializer.
import assert from "node:assert/strict";
function sortPlainJson(value) {
if (Array.isArray(value)) return value.map(sortPlainJson);
if (value && Object.getPrototypeOf(value) === Object.prototype) {
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, sortPlainJson(value[key])])
);
}
return value;
}
const input = {
z: { second: 2, first: 1 },
a: [{ y: 2, x: 1 }, "keep-position"],
};
const sorted = sortPlainJson(input);
assert.equal(
JSON.stringify(sorted),
'{"a":[{"x":1,"y":2},"keep-position"],"z":{"first":1,"second":2}}'
);
assert.notDeepEqual(
sortPlainJson({ list: [1, 2] }),
sortPlainJson({ list: [2, 1] })
);
This helper is useful for controlled JSON-compatible fixtures, not arbitrary JavaScript graphs. Its alphabetical order also interacts with integer-like keys because rebuilding an ordinary object still subjects those keys to JavaScript enumeration rules. Test actual output rather than assuming Object.keys(value).sort() dictates every subsequent serialization position.
For sanitized files, the site's formatter can sort keys for a cleaner review. Use JSON Compare when you need added, removed, and changed paths. Neither tool proves schema validity or defines cryptographic canonicalization.
8. Does the test distinguish stable snapshots from canonical bytes?
Define whether the goal is semantic equality, readable deterministic snapshots, cache-key generation, or standards-based canonical bytes. Each needs different evidence. Test exact text only for a documented representation. For signatures or cross-language hashes, use the required canonicalization scheme and its fixtures rather than an ad hoc recursive key sorter.
RFC 8785 defines the JSON Canonicalization Scheme, including UTF-16-based property sorting and serialization constraints. Key sorting alone is not enough. Number spelling, string serialization, whitespace, supported input, and output encoding also affect bytes.
import assert from "node:assert/strict";
const a = '{"total":1.0,"items":[1,2]}';
const b = '{ "items": [1, 2], "total": 1e0 }';
assert.deepEqual(JSON.parse(a), JSON.parse(b));
assert.notEqual(a, b);
assert.equal(
JSON.stringify(sortPlainJson(JSON.parse(a))),
JSON.stringify(sortPlainJson(JSON.parse(b)))
);
The last assertion demonstrates one local normalization result; it does not certify JCS compatibility. If the output controls a signature, webhook, content-address, or shared cache key, identify the precise protocol and use a maintained conforming implementation. Preserve an unchanged raw request body whenever a provider signs those original bytes.
The JSON canonicalization checks separate raw-body verification, agreed canonicalization, array order, precision, encoding, authentication, and negative fixtures. Keep semantic API assertions separate from cryptographic representation tests so a failure immediately points to the correct boundary.
What should the JSON key-order checklist confirm?
A useful release checklist proves that object order is ignored for semantic comparisons, arrays retain their contract order, JavaScript's index-key rules are understood, mutation and merging do not hide value changes, sorting is recursive but bounded, and exact-byte tests use a named representation scheme rather than accidental serializer output.
Review these outcomes before approving changed fixtures:
| Boundary | Required evidence |
|---|---|
| API value equality | Parsed objects match independent of member order |
| Array behavior | Reordered positions fail when sequence matters |
| JavaScript serialization | String and array-index ordering fixtures pass |
| Mutation and merging | Final values are asserted before exact text |
| Replacer arrays | Recursive omissions and property order are explicit |
| Sorted fixtures | Arrays stay ordered and unsupported types are rejected |
| Snapshots | Formatting and serializer policy are documented |
| Signatures and hashes | Named canonicalization or unchanged raw bytes are tested |
Current search results show recent guides about stable key sorting and recent developer articles about raw JSON tests failing on order, while the repository already covers general comparison and canonicalization. This article fills the narrower gap with executable boundary tests. No reliable public keyword-volume measurement was available, so the title is based on qualitative intent and competitor coverage only.