Skip to content
APIs

4 Microsoft Graph JSON Batch Tests Before Release

·10 min read
4 Microsoft Graph JSON Batch Tests Before Release - APIs guide from JSON Prettifier

Quick answer: Test Microsoft Graph JSON batching at four boundaries: correlate responses by ID, classify individual outcomes, reconstruct only justified retries, and bound the work generated by batches and pagination. Treat the envelope as transport packaging, not a promise that every operation succeeded or that the whole job has finished.

A dashboard receives HTTP 200 and displays a green completion banner. Inside the JSON, one operation succeeded, another was forbidden, and a third was throttled. The network request finished; the application's job did not.

These are client release tests, not a replacement for endpoint-specific permissions or Microsoft documentation. Start with JSON in REST APIs for the surrounding contract. Microsoft's current JSON batching guide defines the wire format; the tests below focus on what your application does with it.

1. Can the client match shuffled responses to the correct work?

Submit distinct operations, deliberately shuffle their fixture responses, and verify that each result reaches the correct application task through its correlation ID. Never pair arrays by position. Reject unexpected or duplicate response identifiers, and keep missing outcomes unresolved rather than quietly treating them as empty data or successful completion.

Use clearly different fixtures: a profile, a calendar result, and a collection page. Identical empty objects make a positional bug difficult to notice. Attach application metadata before sending, such as the screen panel or export checkpoint that owns each operation. Keep that metadata outside the wire payload unless the API explicitly defines a place for it.

Microsoft documents that batch response order can differ from request order. Its SDK batching guidance also provides access to individual responses by ID. Whether you use those helpers or a custom decoder, test the mapping after the adapter has parsed the actual JSON, not just the function that generated the requests.

This executable fixture illustrates strict correlation for numeric-string IDs generated by this application. It is a local decoder test, not a complete Graph client:

import assert from 'node:assert/strict';
const pending = new Map([
  ['1', { panel: 'profile' }],
  ['2', { panel: 'calendar' }]
]);
const envelope = { responses: [
  { id: '2', status: 200, body: { value: [] } },
  { id: '1', status: 200, body: { displayName: 'Sample' } }
] };
const received = new Map();
for (const response of envelope.responses) {
  assert(pending.has(response.id), 'unexpected response');
  assert(!received.has(response.id), 'duplicate response');
  received.set(response.id, response);
}
assert.equal(received.size, pending.size);
assert.equal(received.get('1').body.displayName, 'Sample');
assert.equal(pending.get('2').panel, 'calendar');

Add negative variants that remove one response, repeat an ID, and insert an unknown ID. Those variants test your defensive decoder; they are not claims that the service normally emits malformed envelopes. A failed invariant should preserve enough safe diagnostic context to investigate, without returning the wrong person's data to a screen.

The request builder needs its own contract check. Graph uses unique string IDs, relative operation URLs, and operation-specific headers; requests with a body need an appropriate content type. Inspect the serialized wire payload rather than assuming a configured SDK object guarantees the intended bytes. Choose a deliberately small local ID namespace so retries cannot accidentally overwrite another pending lookup.

Use JSON Viewer to explore a sanitized fixture's nested fields. The tree is useful for spotting a status under the wrong branch; it does not know whether a Graph operation is authorized or correctly correlated. Replace names, addresses, and tokens before sharing fixtures in tickets.

2. Does a successful envelope still expose every failed operation?

Return one successful operation, one permission failure, and one throttled operation inside the same successful batch envelope. Assert separate application outcomes for all three. Read each inner status and structured error code, preserve successful data, and ensure the overall job remains incomplete while any required operation has not reached a terminal outcome.

Model the job independently from the transport request. A transport state such as received describes whether a response arrived. A business state such as export complete describes whether the required data was collected and persisted. Combining those states into one Boolean makes partial failure almost inevitable.

The Microsoft Graph error reference explains structured error codes and warns against program logic that depends on human-readable messages. Test that changing an error message does not change the retry or user-action decision. Display an application-owned explanation rather than exposing a raw developer diagnostic to an end user.

import assert from 'node:assert/strict';
const responses = [
  { id: '1', status: 200, body: { value: ['saved'] } },
  { id: '2', status: 403, body: { error: { code: 'accessDenied' } } },
  { id: '3', status: 429, headers: { 'Retry-After': '4' } }
];
const classify = response => {
  if (response.status >= 200 && response.status < 300) return 'success';
  if (response.status === 429) return 'deferred';
  if (response.status === 403) return 'action-required';
  return 'unclassified';
};
assert.deepEqual(responses.map(classify), [
  'success', 'action-required', 'deferred'
]);
assert.equal(responses.every(r => classify(r) === 'success'), false);

This classifier covers the fixture, not every Graph status. Production code needs endpoint-specific handling for empty successful bodies, deleted resources, transient errors, and authentication recovery. An unknown code should stay visible in diagnostics rather than falling through to success. The JSON API error-response guide discusses keeping machine decisions separate from display text.

If operations depend on earlier operations, include a failed predecessor in an integration test. Graph's documented dependency mechanism can produce a 424 response for dependent work. Do not treat ordering as a database transaction or assume earlier successful effects disappear. Decide what recovery means for the business workflow before combining mutations in one envelope.

For fault injection, Microsoft's Dev Proxy error-simulation guidance offers a vendor-supported starting point. Check the proxy's observed response shape against the service contract and retain deterministic fixtures too. Random failures are useful exploration, but a release test should fail reliably when the partial-outcome logic is broken.

3. Can retries preserve failed-operation intent without replaying success?

Build the retry set from eligible failed operations, preserve their original method, URL, headers, and body, and verify that successful operations are absent. Respect each throttled response's timing and impose attempt and deadline budgets. Treat an unknown transport outcome separately because a missing response does not prove that a mutation never happened.

Microsoft's throttling guidance explains that batch operations are evaluated individually and throttled responses need explicit handling. SDK retry behavior for ordinary requests does not automatically settle failed operations hidden inside a successful batch envelope. Confirm what your actual SDK and adapter do instead of adding an overlapping retry loop by habit.

Keep a durable logical operation record when a job must survive process restarts. Reconstruct the request from that record rather than reusing a stream that may already have been consumed. Test body preservation with a distinctive nested value, not an empty object. Retry tests that only count requests can pass while the second request sends the wrong content.

import assert from 'node:assert/strict';
const original = [
  { id: '1', method: 'GET', url: '/users?$select=id' },
  { id: '2', method: 'PATCH', url: '/users/example-id',
    headers: { 'Content-Type': 'application/json' },
    body: { officeLocation: 'Sample room' } }
];
const results = [
  { id: '1', status: 200 },
  { id: '2', status: 429, headers: { 'Retry-After': '4' } }
];
const eligible = new Set(results.filter(r => r.status === 429).map(r => r.id));
const retry = structuredClone(original.filter(r => eligible.has(r.id)));
assert.deepEqual(retry.map(r => r.id), ['2']);
assert.deepEqual(retry[0].body, { officeLocation: 'Sample room' });
retry[0].body.officeLocation = 'Changed in retry fixture';
assert.equal(original[1].body.officeLocation, 'Sample room');

This fixture proves selection and copying only. It deliberately does not send a mutation or implement a scheduler. A real scheduler must parse the response's timing safely, avoid starting early, handle cancellation, and stop retrying when its job deadline expires. If you group eligible retries together, waiting for the longest required delay is one documented Graph strategy; do not ignore a slower operation's timing to fill a batch.

Add a lost-response scenario to your integration suite. The server may have accepted a write before the connection failed. Retrying all writes blindly can duplicate effects, while abandoning everything can lose intended work. Use the target operation's documented reconciliation and replay guarantees. The idempotency guide explains why a correlation ID alone is not a universal deduplication key.

Separate logical work identity from attempt identity in your test harness. An operation can retain the same business purpose while appearing in a newly constructed envelope. Keep an explicit relationship between the logical task, its current attempt, and the response lookup. Otherwise, a late response from an abandoned attempt can mark a newer task complete or overwrite its diagnostic state. Simulate that ordering with controlled promises: defer the first attempt's completion, start recovery, then release the old result and assert that the state transition follows your documented ownership policy.

Record attempt number, logical operation alias, chosen delay, outcome class, and next-attempt time. Keep access tokens and full response bodies out of routine logs. The useful question during an incident is which work remains and why, not how many envelopes were sent.

4. Does the work queue stay bounded as batches produce more pages?

Test a workload larger than one batch and a collection response that contains a continuation link. Assert that request splitting, envelope concurrency, page traversal, and checkpoint persistence remain bounded independently. Finishing the initial batch must not mark an export complete while its successful collection operations still have unprocessed continuation pages.

Microsoft currently documents a maximum of 20 operations per JSON batch. Its SDK guidance describes automatic splitting, but verify that behavior for the SDK path you actually use. Splitting is not a scheduler: turning a large list into many compliant envelopes can still overload your application if all envelopes start together.

Service-specific Graph throttling limits vary by workload. Avoid treating the envelope count as the only measure of pressure. Track pending operations, active envelopes, retained response bytes, and delayed retries separately. Start with conservative concurrency and measure useful completed work, not just a lower number of HTTP roundtrips.

import assert from 'node:assert/strict';
const operations = Array.from({ length: 41 }, (_, i) => ({ id: String(i) }));
const chunks = [];
for (let i = 0; i < operations.length; i += 20) {
  chunks.push(operations.slice(i, i + 20));
}
assert.deepEqual(chunks.map(c => c.length), [20, 20, 1]);
assert.equal(new Set(chunks.flat().map(r => r.id)).size, 41);
const body = { value: [{ id: 'sample' }],
  '@odata.nextLink': 'https://graph.microsoft.com/v1.0/users?$skiptoken=sample' };
const jobFinished = !body['@odata.nextLink'];
assert.equal(jobFinished, false);

The Graph paging guide directs clients to follow the returned continuation URL until it is absent. Do not invent page tokens or infer completion from the number of rows alone. Follow the documented query and header requirements for the particular endpoint. If you enqueue continuation work through batching, adapt it to the batch URL contract without changing opaque query values; a separate authenticated page request is often simpler.

Checkpoint after the application's chosen persistence boundary. A crash after saving records but before saving the next-page checkpoint should not silently duplicate downstream effects when the job resumes. Test restart behavior with a tiny synthetic dataset, then verify that delayed retries do not starve fresh work or consume the entire queue indefinitely.

Use a queue test that records active work rather than relying on elapsed runtime. Block a few fake envelope sends, enqueue more work than the configured capacity, and assert that the active count never exceeds the limit. Release one send and verify that only the allowed replacement work starts. Then cancel the job while retries are delayed and confirm that cancellation prevents new sends. This gives you deterministic evidence about your scheduler without deliberately exhausting a real tenant's quota.

Finish with an application-level acceptance check. Feed one operation an empty collection with a continuation link, another a terminal empty collection, and a third a throttled response. The first stays queued, the second completes, and the third becomes deferred. The progress display should reflect those different states without promising an exact completion percentage from an unknown final page count. That small fixture catches a surprisingly broad class of premature completion bugs.

For background, batch processing groups work for execution; it does not itself guarantee atomicity, unlimited throughput, or complete pagination. This article's Graph-specific rules rely on Microsoft documentation, not that general definition.

All four JavaScript fixtures were executed locally. They test client invariants, not live Graph permissions, service quotas, or SDK networking. Before deployment, run the same scenarios through your selected SDK against a safe test environment. Keep the pagination checklist and rate-limit checklist beside these fixtures so transport efficiency never hides unfinished work.

Frequently Asked Questions

Does HTTP 200 mean every Microsoft Graph batch request succeeded?
No. The outer HTTP status describes the batch exchange, while each operation has its own status inside the responses array. A client must correlate those entries with submitted requests and classify each outcome. A successful envelope can contain authorization failures or throttled operations that still require explicit handling.
Do Microsoft Graph batch responses preserve request order?
Individual responses can arrive in a different order from the submitted requests. Match each response using its correlation ID rather than its array position. Keep application context in a separate lookup, and test missing, duplicate, and unexpected response IDs so malformed fixtures cannot silently attach one result to another task.
Does batching avoid Microsoft Graph throttling?
No. Operations inside a batch remain subject to applicable service limits. Inspect throttling at the individual response level and respect the supplied retry timing. Combining requests reduces network roundtrips, but it does not grant additional quota or justify launching an unlimited number of batch envelopes at the same time.
Should I retry an entire batch after one operation fails?
Do not replay successful operations merely because a different operation failed. Build a retry set from explicitly eligible outcomes, preserve each operation's original intent, and respect timing and retry budgets. If the entire exchange has an unknown outcome, reconcile potentially completed writes before deciding whether replay is safe.
Can JSON Viewer validate a Microsoft Graph batch integration?
JSON Viewer can help inspect a sanitized batch document as a tree and locate nested status, header, and body fields. It cannot verify Microsoft Graph permissions, execute batch requests, or prove retry safety. Use local fixtures and integration tests for those behaviors, keeping access tokens and personal data out of examples.

Inspect a sanitized batch response

Explore nested operation statuses, headers, and bodies as a JSON tree before adding the fixture to your tests.

Open JSON Viewer