Import JSON files with type: 'json', no fetch() or JSON.parse() needed
Import Attributes let you import JSON files directly as an ES module, with an explicit type hint that became mandatory for security reasons. In many cases it replaces fetch() plus JSON.parse() with a single line.
Table of Contents
- 1. From the fetch() Detour to a Direct Import
- 2. Syntax: import ... with { type: 'json' }
- 3. Dynamic Import With Attributes
- 4. The Security Rationale Behind the Type Attribute
- 5. Comparison: JSON and CSS Module Attributes
- 6. Browser and Node.js Support
- 7. Support in Bundlers and TypeScript
- 8. When fetch(), When Import Attributes?
- 9. Best Practices and Summary
- 10. Summary
- 11. FAQ
1. From the fetch() Detour to a Direct Import
Static configuration data, translation files, or test fixtures live as a JSON file in many projects. Until recently, the browser offered exactly one standard way to load them: call fetch('./data.json'), wait for the response, call .json(), and process the result, all asynchronously with its own error handling for network and parse failures.
Import Attributes introduce a direct, declarative path: import data from './data.json' with { type: 'json' }. The module system itself loads and parses the file, the result is available as the default export, with no manual fetch, no .json() call, and no extra promise layer that unnecessarily complicated plain configuration loading.
2. Syntax: import ... with { type: 'json' }
The syntax builds on existing import statements, adding a with clause at the end. Statically it looks like import config from './config.json' with { type: 'json' }, after which config directly holds the parsed JavaScript object. Without the type attribute, the engine rejects the import with an error, even if the file extension is clearly .json.
The attribute is deliberately redundant with the file extension, because the engine cannot rely on extensions alone: servers can serve JSON content under arbitrary paths without a .json extension, and Content-Type headers are not trustworthy enough from a security standpoint to decide the module kind on their own. The explicit declaration in the source code is the actual anchor of trust.
import config from './config.json' with { type: 'json' };
console.log(config.apiBaseUrl);
// Without the with clause: SyntaxError / TypeError, depending on the engine
// import config from './config.json'; // rejected
3. Dynamic Import With Attributes
For cases where the path is only known at runtime, or the JSON should only load conditionally, the same idea works with dynamic import(). Instead of a top-level statement, a second argument object is passed to import(), whose with property in turn holds the type attribute. The result is a promise that resolves to a module namespace object.
The actual data value sits in the .default property of the resolved module, analogous to the default export in static imports. That makes dynamic JSON imports a real alternative to fetch() for locally bundled data, with the advantage that build tools can already recognize the import as a dependency at build time and bundle it accordingly.
async function loadLocale(code) {
const module = await import(`./locales/${code}.json`, {
with: { type: 'json' },
});
return module.default;
}
const de = await loadLocale('de');
4. The Security Rationale Behind the Type Attribute
The requirement for an explicit type attribute is not a formality, it is a direct response to a real attack scenario. Without that requirement, an attacker who controls the content of a seemingly harmless JSON resource, for example through a compromised CDN response or an open upload endpoint, could try to place executable JavaScript there instead of plain JSON and get it executed via a normal JS import.
With the type attribute, the developer explicitly declares which module kind they expect, and the engine enforces a matching MIME type check against the server's response header. If the actual content type deviates from the expected type, the import fails instead of silently interpreting the file as a script. Historically this feature was called 'Import Assertions' with the assert keyword, but it was renamed to 'Import Attributes' with with due to unwanted error semantics.
5. Comparison: JSON and CSS Module Attributes
JSON is not the only use case for import attributes, conceptually the same syntax also exists for CSS modules via type: 'css', letting a stylesheet be imported as a CSSStyleSheet object instead of text. Support for CSS module scripts, however, currently lags noticeably behind support for JSON modules, especially outside Chromium-based browsers.
In practice that means JSON modules via with { type: 'json' } can already be used broadly in production today, while CSS module scripts are more of a preview of future bundler and framework integration. Anyone using both feature families in their code should check support for each separately rather than assuming uniform availability.
// JSON module, broadly supported
import theme from './theme.json' with { type: 'json' };
// CSS module, experimental / more limited support
import sheet from './styles.css' with { type: 'css' };
document.adoptedStyleSheets = [sheet];
6. Browser and Node.js Support
Chrome and Edge have fully supported static and dynamic JSON import attributes since version 123, Node.js caught up with version 22 and has been stable there since. Safari and Firefox support is currently more uneven: dynamic imports with type: 'json' work more broadly than static top-level imports, which is why the dynamic path is currently recommended for maximum compatibility.
For safe runtime feature testing, a try/catch around a dynamic test import works well, since there is no synchronous way to check support without an actual import attempt. In production code it is also worth adding a fallback to fetch() plus JSON.parse() for environments that do not yet know import attributes.
async function supportsJsonImportAttributes() {
try {
await import('data:application/json,{}', { with: { type: 'json' } });
return true;
} catch {
return false;
}
}
7. Support in Bundlers and TypeScript
Vite and Webpack now natively support the import attribute syntax in current versions and resolve the with clause already at build time, so the bundled output works regardless of the target browser. TypeScript needs at least version 5.3 for the new syntax, together with a suitable module target like 'esnext' or 'nodenext' in tsconfig.json.
Anyone using older bundler versions should check before switching whether the with clause is parsed correctly, since a plain parse-time syntax error (not a runtime one) can abort the entire build. A short isolated test import in a new file is the fastest way to verify that before a larger refactor.
// tsconfig.json (excerpt)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler"
}
}
8. When fetch(), When Import Attributes?
Import attributes suit JSON that is known at build time and should be bundled with the rest of the code, for example configuration files, static translation files, or test fixtures. The advantage: build tools recognize the dependency, can add it to the dependency graph, and can apply tree-shaking or caching strategies to it, exactly like with any other JS import.
fetch() remains the right choice for JSON that is only loaded from a server at runtime, that changes between requests, or that comes from user input or external APIs. Import attributes are not a replacement for network requests, they are a simplification for the narrow case of locally shipped, static JSON files.
// Static, known at build time -> Import Attributes
import featureFlags from './feature-flags.json' with { type: 'json' };
// Dynamic, runtime dependent -> fetch()
const response = await fetch('/api/user-preferences');
const preferences = await response.json();
9. Best Practices and Summary
Import attributes solve a concrete problem more elegantly than fetch() plus JSON.parse() ever could: static JSON becomes a normal module dependency that the build system understands, type-checks, and tracks. The requirement for an explicit type attribute is not syntactic overhead, it is a deliberate security measure against MIME type confusion.
For production use it is currently worth checking the target environment: with modern bundler usage (Vite, Webpack, current TypeScript) the syntax is already safe to use, with direct browser usage without a build step a feature detection with a fetch() fallback is worthwhile for now, until Safari and Firefox catch up on static imports.
| Approach | When Sensible | Build-Time Detection | Support Status |
|---|---|---|---|
| Import Attributes (static) | Known, bundled JSON | Yes | Chrome/Edge/Node stable, Safari/Firefox partial |
| Import Attributes (dynamic) | Conditional loading of JSON modules | Partial | Broader support than static |
| fetch() + JSON.parse() | Runtime/server data, external APIs | No | Available everywhere |
| CSS modules (type: 'css') | Import stylesheets as CSSStyleSheet | Partial | Mostly Chromium browsers |
Mironsoft
Modern browser APIs, performance, and maintainable JavaScript
JavaScript that holds up in the real browser, not just in the tutorial?
We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.
Code Review
Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.
Performance Optimization
Improving bundle size, load time, and runtime performance with modern APIs.
Modernization
Deliberately introducing native browser APIs instead of heavy libraries.
10. Summary
Import Attributes: The Essentials at a Glance
Syntax
import x from './d.json' with { type: 'json' } loads and parses JSON directly as a module, without fetch() or JSON.parse().
Security rationale
The type attribute enforces a MIME type check and prevents externally controlled resources from being interpreted as a script instead of data.
Support
Chrome, Edge, and Node.js support static and dynamic JSON imports stably, Safari/Firefox currently favor the dynamic path.
Scope
Only for JSON known at build time and shipped locally, for runtime data fetch() remains the right choice.