Quick answer: Check four boundaries before deploying a JSON module: the runtime accepts the emitted import attributes, your code reads the default export correctly, the production path actually delivers JSON, and module caching matches the data's freshness needs. Valid JSON alone does not prove that its import will work.
The development server starts. The production build passes. Then the deployed process stops on ERR_IMPORT_ATTRIBUTE_MISSING, or a browser rejects a file that opens perfectly in your editor. Start with the loader contract rather than changing the document's punctuation.
This checklist targets native JSON module loading and the code that build tools leave for a runtime to execute. For general disk reads and writes, use the broader Node.js JSON file guide. Wikipedia's ECMAScript overview supplies language-standard background; runtime behavior below is grounded in primary documentation and executable fixtures.
1. Does the deployed runtime accept the emitted import attributes?
Check the JavaScript that actually executes, not just the source file your editor accepts. Current JSON module imports use the with attribute syntax, while older examples use assert. Match the syntax to your supported runtime, then run the built entry point in that environment before treating a successful compilation as deployment evidence.
MDN's import attributes reference documents the current syntax and identifies the older assertion form as non-standard. Do not switch keywords based only on an error screenshot from another project. Record the exact runtime version and the file named by your own stack trace.
For a reproducible local probe, save the following as an .mjs file and execute it with the Node binary intended for deployment. The data URL keeps the test independent of your project directory and avoids confusing a missing file with a missing attribute:
import assert from 'node:assert/strict';
import settings from 'data:application/json,{"mode":"preview","retries":2}'
with { type: 'json' };
assert.equal(settings.mode, 'preview');
assert.equal(settings.retries, 2);
console.log('Static JSON import passed');
This isolates parsing and loading support. It deliberately does not test a browser, your bundler, or whether your release archive includes a real JSON file. Keep those tests separate so a failure has a useful meaning.
There is concrete community evidence for this diagnostic path. A Yuque MCP server issue opened March 30, 2026 reports a startup failure importing package metadata without the required attribute on Node 24.14.0. It demonstrates one application's failure, not a new requirement introduced for every project by that release.
Build output deserves its own inspection. The Nuxt Icon module source contains an explicit output setting to preserve the intended import-attribute keyword. That is a useful reminder to inspect transformation settings when source and emitted code disagree; copying that setting into an unrelated build system is not a universal fix.
For TypeScript projects, resolveJsonModule documentation describes JSON resolution and inferred types. A type-checking success answers a compiler question. Your deployment test must still answer a runtime question. Record both outcomes in the release notes rather than replacing one with the other.
If package scope is also confusing, review the package.json module settings guide. Use a small explicit .mjs probe to narrow the problem before changing a whole application's module mode.
2. Are you reading the JSON module's default export correctly?
Treat a native JSON module as one exported value, not as a collection of named JavaScript exports. A static default import gives you the parsed value directly. Dynamic import returns a module namespace, so read its default property before accessing document fields, validating their shapes, or passing them into application code.
Node's JSON module documentation specifies a default export and no named exports. An object containing a property called mode does not thereby export a JavaScript binding called mode. Destructuring the object after import is a different operation from requesting a named module export.
Use the distinction as a test, not merely a style preference:
import assert from 'node:assert/strict';
const url = 'data:application/json,{"mode":"preview","limits":{"jobs":3}}';
const namespace = await import(url, { with: { type: 'json' } });
assert.equal(namespace.mode, undefined);
assert.equal(namespace.default.mode, 'preview');
const { mode, limits } = namespace.default;
assert.equal(mode, 'preview');
assert.equal(limits.jobs, 3);
await assert.rejects(
import('data:application/json,{"missingAttribute":true}'),
error => error.code === 'ERR_IMPORT_ATTRIBUTE_MISSING'
);
The final assertion intentionally omits an attribute to check a negative case on the tested Node runtime. Node's error reference identifies the missing-attribute condition. Do not require identical wording from browsers or other runtimes; assert the contract appropriate to the environment under test.
Now add an application-level check. If limits.jobs must be a positive integer, importing the document successfully does not establish that rule. Validate it before using it to schedule work. A JSON module can contain syntactically valid data that makes no sense for your application.
For example, design two fixtures: one with the expected nested object and another with limits set to a string. The loader can accept both, while your configuration validator should reject the second. This prevents an import fix from disguising a separate contract problem. Our JSON Schema introduction covers that additional validation boundary.
Inspect a redacted fixture in the JSON Viewer tree view when the nesting is difficult to follow. The viewer can help you locate limits.jobs; it cannot verify default-export behavior, import attributes, or your application's configuration rules. Keep production credentials out of shared debugging examples.
If a development tool permits named imports from JSON, document that behavior as tool-specific. Test what the shipping artifact contains instead of assuming the development convenience survives native execution unchanged.
3. Does the production path deliver the JSON asset correctly?
Test loading from the built application's real location and delivery path. A file available beside source code can be absent from the release directory, and a browser URL ending in .json can return an HTML fallback. Check asset inclusion, resolution, response status, and media type independently before changing import syntax again.
MDN's JavaScript module guide explains browser module delivery, MIME checking, and local-server requirements. Run a browser deployment test over HTTP rather than assuming that opening an HTML file directly reproduces hosted module loading. Inspect the failing request in the network panel.
Build a deliberately boring release fixture. Place one non-secret JSON document beside the emitted entry point. Start the application from a different working directory, then request the deployed page through the normal reverse proxy. That combination catches assumptions hidden by a development server's convenient project root.
For browser delivery, examine the final response after redirects. A successful status alone is insufficient: single-page application fallbacks commonly serve HTML for unknown paths. Compare the actual response with the intended JSON resource, and confirm the server declares the JSON media type. The JSON Content-Type guide explains that separate HTTP contract.
The following executable fixture tests an explicit fetch loader, not native browser imports. It shows why checking the response boundary is useful when you choose fetch as an alternative for runtime data:
import assert from 'node:assert/strict';
async function readJsonResponse(response) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const type = (response.headers.get('content-type') || '')
.split(';')[0].trim().toLowerCase();
if (type !== 'application/json') throw new Error('Expected application/json');
return response.json();
}
const good = new Response('{"ready":true}', {
headers: { 'content-type': 'application/json; charset=utf-8' }
});
assert.deepEqual(await readJsonResponse(good), { ready: true });
await assert.rejects(readJsonResponse(new Response('<html>Fallback</html>', {
headers: { 'content-type': 'text/html' }
})), /Expected application\/json/);
await assert.rejects(readJsonResponse(new Response('', { status: 404 })), /HTTP 404/);
This intentionally narrow loader accepts application/json, not every possible JSON-based media type. Adapt the accepted types to your endpoint's documented contract. Response.json documentation describes body parsing; successful parsing is still separate from checking status, content type, and application fields.
Native imports are not configurable fetch wrappers. If your application needs request-specific authorization, refresh controls, or specialized response handling, decide explicitly whether a data-loading API fits better. Do not strip safeguards merely to make an import error disappear.
Finally, test your supported browser matrix and production build together. The local Node fixtures here cannot establish browser compatibility or prove that an asset survived your hosting platform's packaging rules.
Keep an asset manifest with the expected relative path, destination URL, and deployment check for each imported document. For a broken release, compare that manifest with the uploaded archive before rebuilding dependencies. If the file is missing, fix packaging; if the URL returns your application shell, fix routing. Those are different changes with different owners and rollback plans.
4. Does module caching match how often the JSON must change?
Use JSON imports for module-like data whose lifetime matches the application instance, not as an automatic refresh mechanism. Repeated successful imports of the same module identity reuse module state. For changing configuration or live records, choose an explicit read or request and define freshness, validation, and failure behavior at that data boundary.
MDN's dynamic import reference documents namespace caching and warns against repeatedly creating unique module URLs to bypass it. Importing the same resource again is not a dependable instruction to reread a file. HTTP caching and module identity are also different layers; changing one does not automatically reset the other.
This fixture makes shared identity visible without editing any disk files:
import assert from 'node:assert/strict';
const url = 'data:application/json,{"flags":{"search":false}}';
const first = (await import(url, { with: { type: 'json' } })).default;
const second = (await import(url, { with: { type: 'json' } })).default;
assert.strictEqual(first, second);
const local = structuredClone(first);
local.flags.search = true;
assert.equal(second.flags.search, false);
first.flags.search = true;
assert.equal(second.flags.search, true);
The assertions demonstrate two different choices. Cloning protects this fixture's original object from a local edit. Mutating the shared imported object changes what another consumer sees. Neither choice refreshes the source, and cloning does not turn an imported configuration into a live settings service.
Write down ownership before shipping. Does the importing module expose the original value, a copy, or a validated application-specific projection? Who is allowed to change it? Tests become clearer when that decision is deliberate rather than an accidental consequence of passing a nested reference around.
Include a restart scenario in the acceptance test: start with one fixture, change the deployed data through your normal release process, and observe the behavior before and after restarting the application. Record what users should expect during that interval. This makes the chosen freshness contract observable instead of relying on a comment that nobody has verified.
For changing local files, Node's filesystem documentation provides explicit asynchronous reads. Combine a read with parsing and validation according to your requirements. The existing Node JSON read/write guide covers path anchoring and write safety; this checklist does not replace those concerns.
For remote data, define when to request again, what cached response is acceptable, and what the interface shows if refreshing fails. The Fetch JSON guide is the appropriate next step. Avoid moving a refresh problem into a dynamic import loop merely because import already returns a promise.
Before release, keep four pieces of evidence: a syntax probe on the target runtime, export-shape assertions, a production asset-loading check, and a documented freshness test. All four executable examples in this article were checked locally with Node.js 24.19.0. Browser loading and your deployed delivery path still require environment-specific testing.