Skip to content
JavaScript

8 JSON Key Order Tests Before Comparing API Responses

·12 min read
8 JSON Key Order Tests Before Comparing API Responses - JavaScript guide from JSON Prettifier

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:

BoundaryRequired evidence
API value equalityParsed objects match independent of member order
Array behaviorReordered positions fail when sequence matters
JavaScript serializationString and array-index ordering fixtures pass
Mutation and mergingFinal values are asserted before exact text
Replacer arraysRecursive omissions and property order are explicit
Sorted fixturesArrays stay ordered and unsupported types are rejected
SnapshotsFormatting and serializer policy are documented
Signatures and hashesNamed 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.

Frequently Asked Questions

Does key order matter in JSON objects?
JSON object-member order does not define the object's meaning, so consumers should normally access values by name and compare parsed structures. Array order is different and remains significant. Key order can still matter to text diffs, snapshots, hashes, signatures, and presentation, but those uses require an explicit representation contract.
Does JSON.stringify preserve object key order?
For ordinary JavaScript objects, JSON.stringify uses the stable Object.keys ordering rules. Array-index keys come first in ascending numeric order, followed by other string keys in property-creation order. That makes repeated serialization of one unchanged object stable, but equivalent objects created in different orders can still produce different JSON text.
Why do numeric JSON keys move to the front in JavaScript?
After parsing, JSON object names become JavaScript property keys. Names that qualify as array indexes are enumerated first in ascending numeric order, regardless of their source position. Other string keys follow their creation order. Treat identifiers as object names only when that representation is appropriate; use arrays when sequence itself carries meaning.
Should tests compare JSON strings or parsed objects?
Compare parsed objects when the contract concerns JSON values, because raw strings also differ on whitespace, key order, escape spelling, and number spelling. Compare exact strings only when bytes or formatting are genuinely contractual, such as a documented snapshot format, canonical representation, signature input, or golden serialization fixture.
Is recursively sorting JSON keys the same as RFC 8785 canonicalization?
No. Recursive key sorting can make local diffs and snapshots deterministic, but RFC 8785 also defines property ordering, number serialization, string handling, whitespace, and input constraints. Use a conforming implementation and official fixtures when a protocol requires JCS; do not advertise a short sorting helper as standards-compliant canonicalization.

Compare the Final JSON Structures

Paste two sanitized responses into JSON Compare to inspect structural changes without treating object-key order as a data change.

Open JSON Compare