Test large IDs by checking which record comes back
Imagine a client change that converts an ID through Number(id): a lookup selects the neighboring record, but the server copies the requested ID into the response. An exact-ID assertion passes even though the wrong record came back.
Test with 9007199254740992 and 9007199254740993, attached to records with different markers. Converting these decimal strings through JavaScript’s Number type collapses them to the same value. Send both through the real client and API lookup path, then verify that each returns its unchanged ID and its own record marker.
I would accept the change only when those checks pass through the implementation being changed, not merely through a helper that serializes strings.
Start with the conversion that breaks the requirement
This JavaScript counterexample exposes the problem:
const first = "9007199254740992";
const second = "9007199254740993";
Number(first) === Number(second); // true
String(Number(second)); // "9007199254740992"
JavaScript’s Number uses binary floating-point representation. At this magnitude, it cannot represent every adjacent integer. The first value is representable, but the next integer rounds to that same value.
Converting the rounded number back to a string does not recover the original ID. The information has already been lost.
A response can have the right type and still contain the wrong ID.
That is why an assertion such as typeof result.id === "string" is useful but insufficient. Compare the exact value, then verify which record was retrieved.
Turn “preserve IDs exactly” into a boundary contract
The example API chooses this policy: record IDs are nonempty strings of decimal digits, their characters must be preserved exactly, and numeric JSON IDs must be rejected before lookup. Numeric-input rejection is a policy choice, not a universal requirement for exact ID handling.
Use these ID identity checks when reviewing the change:
| Signal | Required behavior | What it catches |
|---|---|---|
| Boundary representation | JSON request and response IDs are strings | Numeric IDs entering or leaving the API |
| Exact round trip | Each returned ID equals its requested ID, character for character | Rounding or normalization visible in the result |
| Record distinction | Each ID returns its own fixture marker | A lookup that selects the neighboring record |
| Numeric-input rejection | A JSON number in the ID field is rejected before lookup | A boundary that silently accepts potentially rounded input |
Send an ID as:
{"id":"9007199254740993"}
Do not send it as a JSON number and expect the receiver to reconstruct the original text. A parser that converts that number to a floating-point value can discard the distinction before application validation begins.
Reject numeric IDs rather than repairing them after parsing.
Strings are the boundary representation here. An implementation can use an exact integer type internally if it preserves the contract, but converting through a lossy numeric type is unacceptable.
Test two records through the actual lookup path
Create two records in an isolated test database or fixture store. Give them different non-ID markers so the test checks record selection as well as response formatting.
Keep these fixtures in a shared JSON file if several implementations need the same cases:
[
{
"id": "9007199254740992",
"marker": "record-a"
},
{
"id": "9007199254740993",
"marker": "record-b"
}
]
If a lookup for the second ID selects record A, copying the requested ID into the response can hide the mistake. Its record-a marker still exposes it.
An exact returned ID does not establish which record was selected.
The following JavaScript acceptance helper checks both:
import assert from "node:assert/strict";
export async function checkRecordIdentity(readRecord, fixtures) {
assert.equal(fixtures.length, 2);
assert.notEqual(fixtures[0].id, fixtures[1].id);
assert.notEqual(fixtures[0].marker, fixtures[1].marker);
for (const expected of fixtures) {
const actual = await readRecord(expected.id);
assert.equal(typeof actual.id, "string");
assert.equal(actual.id, expected.id);
assert.equal(actual.marker, expected.marker);
}
}
// Adapter placeholder: replace this call with your real client's
// method and argument shape. Point it at the isolated API and
// seed both fixture records before running the check.
const readRecord = (id) => realClient.readRecord({ id });
await checkRecordIdentity(readRecord, fixtures);
The request must pass through the client’s request construction, serialization, server parsing, database lookup, response serialization, and client response parsing.
Do not replace that path with a map lookup or a mock that simply echoes the requested ID. Those substitutes bypass the conversions the test is meant to catch.
Check the outbound JSON and numeric-input rejection
In your integration harness, capture the actual outbound request body at the transport boundary, after serialization. Parse that captured JSON and assert that its id is a string equal to the requested fixture ID. Inspecting an object before serialization misses changes made by the serializer or later request-building code.
Separately, send a raw JSON request containing a numeric ID directly to the API, bypassing any client validation. Assert the API’s documented validation response and, where the harness allows it, that lookup was not invoked. Choose and document the expected rejection behavior rather than inventing a status code inside the test.
For multiple language implementations, reuse the same JSON fixtures and acceptance expectations. Each implementation should load the IDs as strings and exercise its actual serializer, parser, and storage adapter. Shared fixtures provide common cases, not identical execution paths.
Keep the contract assertions under review alongside the implementation diff. A passing test loses value if the change also replaces the distinct fixtures with small, safely representable IDs or weakens the assertions.
Add the pair to an existing integration test
In five minutes, add the two isolated fixtures to your existing client integration harness. Run both lookups and assert the exact ID and distinct marker for each. I would use this pair to gate a client or API change that handles large IDs, alongside the endpoint’s broader tests.
Passing the two-record test gives evidence that these values remain distinct on the exercised path; numeric-input rejection needs its separate raw-request check. The pair does not cover longer IDs, malformed input, length limits, authorization, database migration correctness, or separate bulk-import, export, cache, and event paths. Whether leading zeros are valid remains a separate API decision, and these fixtures do not test it.