Skip to content
Advanced

5 Sparse Array JSON Tests Before JavaScript Release

·10 min read
5 Sparse Array JSON Tests Before JavaScript Release - Advanced guide from JSON Prettifier

Quick answer: Test sparse arrays before JSON serialization by checking slot ownership, skipped validation callbacks, removal behavior, attached metadata, and the transport representation chosen for missing positions. Valid JSON can hide an in-memory change: a hole, an explicit undefined value, and null can all produce the same array element in ordinary output.

A batch editor deletes one row, then sends a null where the server expected either a real row or no row at all. The array versus object guide explains container choice; these five tests isolate JavaScript array presence and its JSON boundary.

Run the JavaScript blocks together, in order, with Node.js v24.22.1, the runtime used for these local synthetic fixtures. No browser matrix or remote endpoint was tested. Arrays here are ordinary data containers, without custom toJSON methods or getters unless stated. Wikipedia's serialization overview supplies background; the language references define the behavior below.

1. Do holes, undefined elements, and null remain distinguishable after serialization?

Test slot ownership before encoding and compare the parsed result afterward. In ordinary arrays without inherited indexed values, holes and undefined elements serialize as null, just like explicit null elements. The JSON text therefore cannot recover those original distinctions. Treat the round trip as a representation change whenever absence carries meaning in the application contract.

The ECMAScript array serialization algorithm visits positions through the array length. The Object.hasOwn reference provides an ownership test that distinguishes a missing own slot from an own property whose value is undefined.

const assert = require("node:assert/strict");
const hole = new Array(1);
const explicitUndefined = [undefined];
const explicitNull = [null];
assert.equal(Object.hasOwn(hole, 0), false);
assert.equal(Object.hasOwn(explicitUndefined, 0), true);
assert.equal(Object.hasOwn(explicitNull, 0), true);
for (const value of [hole, explicitUndefined, explicitNull]) {
  assert.equal(JSON.stringify(value), "[null]");
  const parsed = JSON.parse(JSON.stringify(value));
  assert.equal(Object.hasOwn(parsed, 0), true);
  assert.equal(parsed[0], null);
}

Comparing only serialized strings would declare these inputs equivalent. That may be acceptable for a deliberately normalized API, but it does not establish equivalence for a UI model that tracks unfilled positions separately from explicitly cleared positions. Decide whether the server needs that distinction before choosing a JSON array as the representation.

Keep an explicit null fixture even if the current producer uses holes only. It catches a future assumption that every received null must mean “this index was absent.” The receiver has no such evidence from the ordinary JSON text. The null versus undefined guide covers the broader container differences without restoring information already lost during encoding.

These examples assume clean ordinary prototypes. Indexed property lookup can observe inherited values, so a missing own property is not a universal promise that reading that index returns undefined. Ownership assertions are useful precisely because they state what the application checks rather than conflating absence with a particular read result.

Retain a fixture with a trailing hole, not just a missing middle element. A sparse array's length can extend past its last present index, and serialization still represents the positions through that length. A helper that inspects only enumerable keys can therefore underestimate the outgoing array's number of elements. Compare length, ownership, and parsed output as separate assertions when adapting this suite to a real batch format.

2. Does preflight validation visit every position the API requires?

Test a sparse array against the same predicate used by the application, then separately check every required index for an own property. Methods such as every skip empty slots, so passing a value predicate does not prove density. A dense-array policy needs both a presence check and a value check, with explicit handling for undefined and null.

MDN's every documentation explains its empty-slot behavior. A preflight validation routine that looks complete in review can therefore evaluate fewer elements than the array's length suggests.

const quantities = [2, , 6];
let visits = 0;
assert.equal(quantities.every(value => {
  visits += 1;
  return Number.isInteger(value) && value > 0;
}), true);
assert.equal(visits, 2);

function densePositiveIntegers(values) {
  if (!Array.isArray(values)) return false;
  for (let index = 0; index < values.length; index += 1) {
    if (!Object.hasOwn(values, index)) return false;
    if (!Number.isInteger(values[index]) || values[index] <= 0) return false;
  }
  return true;
}
assert.equal(densePositiveIntegers(quantities), false);
assert.equal(densePositiveIntegers([2, undefined, 6]), false);
assert.equal(densePositiveIntegers([2, null, 6]), false);
assert.equal(densePositiveIntegers([2, 4, 6]), true);
assert.equal(densePositiveIntegers([]), true);

The empty array is accepted intentionally because this helper defines density and positive integer values, not a minimum item count. Add a separate cardinality rule if an empty order is invalid. Keeping that boundary visible avoids turning a narrowly useful teaching helper into an undocumented complete request validator.

An own-property check also rejects a hole filled only through the prototype. That is a deliberate policy for this plain-data contract. It is stronger than using the in operator, which includes inherited properties. If the application accepts array subclasses, proxies, or accessors, establish a separate normalization boundary rather than claiming this small loop proves side-effect-free inspection.

Do not validate only before a transformation that can create holes or change values. Place a check close to the outgoing representation, and preserve an accepted dense control in the suite. A helper that rejects every array would otherwise pass all of the negative assertions while blocking legitimate requests.

3. Does removing an entry preserve the intended meaning of later indices?

Test delete and splice on copies of the same dense array and compare length, ownership, and JSON output. delete removes an indexed property while retaining length and later positions; splice removes a position and shifts subsequent entries. Neither behavior is universally correct when array indices identify business slots, so assert the intended update semantics explicitly.

The delete operator documentation distinguishes creating an empty slot from changing array length. This matters for scheduled slots, ranked choices, and batch operations where another system interprets a position as an identity rather than just a display order.

const removedProperty = ["A", "B", "C"];
delete removedProperty[1];
assert.equal(removedProperty.length, 3);
assert.equal(Object.hasOwn(removedProperty, 1), false);
assert.equal(removedProperty[2], "C");
assert.equal(JSON.stringify(removedProperty), '["A",null,"C"]');

const removedPosition = ["A", "B", "C"];
removedPosition.splice(1, 1);
assert.equal(removedPosition.length, 2);
assert.equal(removedPosition[1], "C");
assert.equal(JSON.stringify(removedPosition), '["A","C"]');

If slot two belongs to a particular time period, shifting C into an earlier slot can be worse than sending a null. If the array is simply a list of active tags, retaining a hole may be the mistake. The encoder cannot resolve that product decision. Use stable record identifiers when identity should survive list edits.

Test downstream patch behavior separately from these local operations. A JavaScript delete expression is not a JSON Patch remove operation, and a null element is not automatically an instruction to delete something on the server. The JSON Patch array update tests cover operation-level index changes where the protocol explicitly defines them.

Also avoid filter as an unexplained cleanup step. Filtering can compact an array and change every later position, while a truthiness predicate can drop valid zero or false values as well. Choose a specific normalization rule and assert the correspondence between source indices and transmitted records, rather than treating shorter output as evidence that the data was repaired.

For an editor with undo support, preserve the operation's intent alongside its effects. A user action that clears a fixed slot and one that removes an item from an ordered list may produce similar screen layouts but require different server requests. Include the original record identifiers and expected positions in integration fixtures, so a refactor cannot silently convert one editing model into the other.

4. Do array metadata and normalization survive the outgoing shape deliberately?

Test named properties attached to an array separately from indexed values, and test whether spread or Array.from changes slot ownership before serialization. Ordinary array JSON output omits attached named metadata. Normalization can turn holes into present undefined elements while leaving the eventual JSON unchanged, so inspect both the in-memory structure and the final transport envelope.

The JSON.stringify reference explains indexed array serialization. The Array.from documentation describes how absent indices become undefined entries rather than preserved holes in ordinary array-like conversion.

const rows = ["A", , "C"];
rows.batchId = "batch-demo";
assert.equal(Object.keys(rows).includes("batchId"), true);
assert.equal(JSON.stringify(rows), '["A",null,"C"]');

const spreadRows = [...rows];
const copiedRows = Array.from(rows);
for (const copy of [spreadRows, copiedRows]) {
  assert.equal(Object.hasOwn(copy, 1), true);
  assert.equal(copy[1], undefined);
  assert.equal(JSON.stringify(copy), JSON.stringify(rows));
}
const envelope = { batchId: rows.batchId, rows: ["A", "B", "C"] };
assert.deepEqual(JSON.parse(JSON.stringify(envelope)), {
  batchId: "batch-demo", rows: ["A", "B", "C"]
});

The named property is enumerable and still disappears from the ordinary array JSON. That is why checking Object.keys alone is not enough to prove which properties reach the receiver. An explicit object envelope makes batch metadata part of the contract instead of relying on an array's additional JavaScript properties.

Use the JSON Viewer to inspect the completed sanitized envelope. Expand rows and confirm that batchId is a sibling field rather than information you expected to find on the array itself. The viewer cannot reconstruct source holes, execute these ownership checks, or establish whether the server accepts the envelope's schema.

Be careful when comparing normalized and original arrays in logs or snapshots. Equal JSON strings in this fixture conceal a real ownership difference before encoding. That difference can change later callback execution even though the first transmitted payload looks identical. Test the operation that consumes the in-memory array as well as the final JSON representation.

5. Does an explicit indexed representation preserve absence when the API needs it?

Test a transport shape that records present entries and total logical length when missing positions must remain meaningful. Do not expect ordinary JSON arrays to preserve holes. Define which values are allowed in present entries, reject unsupported values before packing, and verify both occupied indices and absent indices after reconstructing a controlled round trip.

An indexed envelope is an application protocol, not a special JSON feature. The following compact fixture supports only strings in present slots, which keeps value preservation separate from representing missing positions. A general-purpose serializer would need additional policies for nested values, duplicates, invalid indices, and untrusted envelopes.

function packStringSlots(values) {
  const entries = [];
  for (let index = 0; index < values.length; index += 1) {
    if (!Object.hasOwn(values, index)) continue;
    if (typeof values[index] !== "string") throw new TypeError("Expected string slot");
    entries.push({ index, value: values[index] });
  }
  return { length: values.length, entries };
}
const original = ["A", , "C"];
const packed = JSON.parse(JSON.stringify(packStringSlots(original)));
assert.deepEqual(packed, {
  length: 3, entries: [{ index: 0, value: "A" }, { index: 2, value: "C" }]
});
const restored = new Array(packed.length);
for (const entry of packed.entries) restored[entry.index] = entry.value;
assert.equal(restored.length, 3);
assert.equal(Object.hasOwn(restored, 1), false);
assert.equal(restored[2], "C");
assert.throws(() => packStringSlots([undefined]), TypeError);
assert.throws(() => packStringSlots([null]), TypeError);
console.log("All five sparse-array test groups passed");

The reconstruction uses only the trusted envelope produced by this test. Do not expose it as an unchecked decoder for arbitrary network input: an excessive length or out-of-range index can create resource and integrity problems. Validate the envelope's schema, uniqueness of indices, limits, and value rules before reconstruction in a real implementation.

If preserving sparse positions is unnecessary, rejecting holes before sending a dense JSON array is simpler. Document that choice and test it. The useful result of these five groups is a clear boundary between JavaScript presence, serialization, and the server contract, with executable evidence for each rather than a blanket claim that every successful JSON round trip preserves the original data.

When writing a regression report, include the construction step that produced the array. A literal with an empty slot, an indexed delete, and a dense array containing undefined are different starting points even when the output text matches. Record the runtime and exact assertions, then keep the fixture small enough that another developer can reproduce the presence change without a UI or real customer data. Avoid inferring missing-slot history from a received null: that history was never carried by the ordinary JSON array.

Frequently Asked Questions

Does JSON preserve holes in a JavaScript array?
Ordinary JSON.stringify output does not preserve an absent array slot as a hole. In a controlled array without inherited indexed values or custom serialization, a hole becomes null, and parsing creates a present element. If absence matters, reject sparse inputs or define an explicit indexed representation before sending the payload.
Can Array.every miss holes during validation?
Yes. every does not call its predicate for empty slots, so a sparse array can pass a value predicate without every index being checked. Validate index ownership separately when the contract requires dense arrays, then validate the present values. Include both a sparse fixture and a dense accepted control in tests.
Does delete remove an array position?
Deleting an indexed property creates a hole without shifting later positions or reducing the array's length. splice can remove an element and shift following indices instead. Choose according to whether positions carry identity, and assert the final serialized payload rather than assuming both operations produce the same API update.
Will JSON.stringify include custom properties attached to an array?
Ordinary array serialization visits indexed positions within the array length rather than serializing arbitrary named properties attached to the array object. Extra metadata can therefore disappear even when it is enumerable. Put transport metadata in an explicit surrounding object and test that envelope alongside the indexed values it describes.
Does spreading a sparse array preserve missing slots?
For an ordinary sparse array with no inherited indexed values, spreading materializes the visited missing positions as present undefined elements. That changes ownership before serialization, while JSON output still uses null for those positions. Test ownership directly when comparing in-memory arrays; equal JSON strings do not prove that the original arrays had identical presence semantics.

Can you inspect the array sent over the wire?

Use JSON Viewer to expand the completed, sanitized payload after the JavaScript assertions pass.

Open JSON Viewer