Modern JavaScript applications increasingly load more than JavaScript: JSON configuration, CSS, WebAssembly, and occasionally other data modules. Import attributes let an application state what it expects to load at the import site, so the runtime can validate that expectation before the module is used. For teams building with Node.js, browsers, bundlers, and frameworks, the practical win is clearer intent and a safer replacement for legacy JSON import assertions.
TL;DR: Use with { type: 'json' } when importing JSON modules, make your build tools preserve it, and treat the attribute as part of the module contract. It is not a generic metadata channel: today, the useful interoperable case is declaring a JSON module’s type.Table of contents
- What are JavaScript import attributes?
- Why do JSON modules need an explicit type?
- How do import attributes work in code?
- How should a Node.js project migrate?
- What changes for browsers, bundlers, and frameworks?
- Which mistakes cause production failures?
- How does this fit into a safer module strategy?
What are JavaScript import attributes?
Import attributes are a standard ECMAScript syntax for attaching an explicit expectation to a static or dynamic module import. The syntax uses with, not the older assert keyword:
import settings from './settings.json' with { type: 'json' };
const dictionary = await import('./locales/en.json', {
with: { type: 'json' },
});The type attribute tells the host environment how the fetched resource must be interpreted. If ./settings.json is not served or resolved as JSON, the import should fail rather than silently treating an unexpected resource as executable JavaScript. The TC39 import attributes proposal explains the design goal: a module specifier alone does not always communicate enough about the resource being loaded.
This is deliberately narrower than many developers first expect. It is not a replacement for arbitrary loader options, feature flags, or application metadata. Host environments decide which attributes they support and reject unknown or invalid ones. That constraint is good: the statement in source code remains meaningful across tooling rather than becoming a bag of build-time hints.
The change is especially relevant during an ESM migration. Teams that are already adopting native TypeScript execution in Node should keep module syntax boring and explicit; Node.js native TypeScript is much easier to operate when the runtime contract is visible in the import itself.
Why do JSON modules need an explicit type?
JSON is data, but a URL or file path does not inherently prove that it will be delivered as data. On the web, an endpoint can be misconfigured, redirected, or compromised. If a module loader were willing to interpret a response as JavaScript merely because it appeared in an import statement, a data import could become an execution boundary.
The explicit type gives the host a chance to compare intent with the resource it received. Browsers document JSON modules as an import-attributes use case, and the MDN import reference notes that attributes can enforce a module type. Node similarly documents JSON modules as requiring with { type: 'json' } in its ECMAScript modules guide.
There is another operational benefit: readers can distinguish code dependencies from data dependencies immediately. That matters in security review, code generation, and dependency tooling. It echoes the broader lesson behind package provenance: the supply chain is not just packages; it includes the assumptions a runtime makes while loading resources.
How do import attributes work in code?
For a static import, put with { type: 'json' } after the module specifier:
import featureFlags from './feature-flags.json' with { type: 'json' };
if (featureFlags.checkoutV2) {
enableCheckoutV2();
}For a dynamic import, put the same information in the second options argument. Notice that the key is with:
export async function loadLocale(locale) {
const supported = new Set(['en', 'el']);
if (!supported.has(locale)) throw new Error('Unsupported locale');
const module = await import(`./locales/${locale}.json`, {
with: { type: 'json' },
});
return module.default;
}Validate dynamic path segments before constructing the specifier. Import attributes protect the resource type; they do not turn a user-controlled path into a safe allowlist. The example permits only known locales, then requests a JSON module.
The imported JSON value is exposed through the module’s default export. Do not assume every runtime has identical caching, mutability, or interop behavior beyond its documented implementation. If an application needs a fresh copy of a configuration object, clone the data intentionally instead of relying on module-loading quirks.
How should a Node.js project migrate?
Start by finding legacy JSON assertions:
// Legacy syntax: replace it when your supported runtime/toolchain is ready.
import config from './config.json' assert { type: 'json' };Then migrate to the standardized form:
import config from './config.json' with { type: 'json' };Use a small rollout checklist:
- Set a real runtime floor. Check the Node releases used locally, in CI, containers, and serverless deployments. Syntax support in a developer laptop does not guarantee production support.
- Upgrade the parser path. TypeScript, ESLint, test runners, coverage tools, and any source transformer must all parse the new syntax. A failure may look like an application error but actually be an outdated parser.
- Test emitted output. If a bundler transpiles source, inspect a production build and run it in the target runtime. Some tools externalize JSON; others inline it.
- Keep CommonJS boundaries intentional.
require('./config.json')has different semantics and may be necessary in an older CommonJS module. Do not mix styles just to avoid deciding which module system owns a package. - Remove compatibility branches on a schedule. Dual
assert/withcode is temporary debt. Record the runtime floor and delete it once the fleet has moved.
Node’s module documentation is the source of truth for the exact version behavior your deployment needs. Avoid copying a compatibility snippet from an old blog post: this syntax has evolved, and assert is not the direction the standard took.
What changes for browsers, bundlers, and frameworks?
Browsers load JSON modules only when the server response and import declaration satisfy their rules. Test this on the actual origin, not just a development server. A CDN that changes Content-Type, a service worker that rewrites responses, or a route fallback that returns HTML can all turn a working local import into a production error.
Bundlers add a separate question: are they preserving a native import, converting JSON to an object literal, or handling it through a plugin? Any of those can be valid, but the choice should be explicit. A library intended to run directly in browsers and Node should avoid assuming that an application bundler will erase unsupported syntax.
For framework code, prefer regular fetches when the data is truly runtime data—personalized configuration, CMS content, or a frequently changing remote document. JSON modules work best for versioned assets that are part of the deployed module graph. This distinction helps avoid accidental caching problems and pairs well with Next.js cache components: module loading is not a substitute for a deliberate data-cache policy.
Which mistakes cause production failures?
The recurring mistakes are straightforward:
- Keeping
assertforever. It may work in some toolchains, but it is legacy syntax. Move towardwithafter confirming your target environments. - Using arbitrary attributes.
with { purpose: 'config' }is not portable. Attributes are host-defined, so use only documented values. - Skipping the attribute for JSON. In Node ESM, that is an error; in browsers, it can undermine the type check the feature provides.
- Treating dynamic imports as input validation. Validate the module specifier separately and restrict it to expected values.
- Testing only development mode. Production transforms, edge runtimes, and CDNs often use a different loader path.
A useful CI test imports one representative JSON module from the built artifact using the same Node version or browser automation target used in deployment. Keep it beside existing build tests rather than waiting for an integration environment to reveal a syntax issue.
How does this fit into a safer module strategy?
Import attributes are a small language feature with a bigger architectural message: resource types should be explicit at boundaries. They complement other browser hardening measures, not replace them. For example, Integrity-Policy deals with controls around third-party script integrity, while import attributes clarify what a first-party module import expects to receive.
For most teams, the right plan is modest:
- Use
with { type: 'json' }for JSON ESM imports. - Pin and test the runtimes that execute the syntax.
- Keep dynamic module specifiers on an allowlist.
- Choose
fetchinstead when the resource is runtime data rather than a build asset. - Review module loading alongside CSP, dependency provenance, and cache behavior.
That is enough to gain a meaningful correctness and security signal without turning a syntax migration into a rewrite. As the web platform adds more module types, teams that already treat imports as contracts will be easier to evolve.
Frequently asked questions
Are import attributes the same as import assertions?
No. Import assertions used assert { type: 'json' }; the standardized direction is import attributes using with { type: 'json' }. Check the documentation for every runtime you support before removing a temporary compatibility path.
Can I use import attributes to pass custom values to my bundler?
Not portably. Attributes are interpreted by the module host, and unsupported attributes can cause an import to fail. Use a documented bundler plugin or configuration mechanism for build-specific metadata.
Should remote API JSON use an import attribute?
Usually no. Use fetch, validate the response, and apply an explicit cache policy for API data. JSON modules are better suited to assets that belong to the module graph.
Do import attributes replace content security policy?
No. They make the expected module type explicit. CSP, Subresource Integrity or Integrity-Policy, dependency controls, and careful server headers each solve different parts of the loading-security problem.