To import JSON in ES modules you add an import attribute: import data from './data.json' with { type: 'json' }. Node.js 22.12 and later, every current browser and TypeScript 5.3 and later accept that syntax. The attribute is mandatory, the module has only a default export, and the older assert keyword no longer parses in Node 22.
import config from './config.json' with { type: 'json' };
console.log(config.version);
// Dynamic form: the attributes go in a second argument
const { default: locale } = await import('./en-GB.json', {
with: { type: 'json' },
});
That is the whole feature. Three errors stand between most people and that working import, and there is one case where require is still the better tool.
Why does Node need an import attribute of “type: json”?
Leave the attribute off and Node 24.12 fails with this before running a line of your code:
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///app/data.json" needs an import attribute of "type: json"
The reason is a browser problem that Node chose to copy. In a browser, a module’s type is decided by the Content-Type header the server sends, not by the .json in the URL. The MDN page on import attributes spells out the attack: if a server is hijacked and starts serving a “JSON” file as text/javascript, a plain import would execute it as code. The type: 'json' attribute tells the browser to fail the import unless the response really is JSON. Without the attribute, a browser treats every module as JavaScript, and a JSON response fails with Expected a JavaScript module script but the server responded with a MIME type of "application/json".
Node has no Content-Type to check, so the attribute is pure declaration there. The Node ESM docs still make it mandatory so that code behaves the same in both places, and Node throws ERR_IMPORT_ATTRIBUTE_MISSING (added in 21.1.0) instead of guessing from the extension.
Node also rejects values it does not understand. Only type: 'json' is supported, plus type: 'text' behind --experimental-import-text. Anything else, say with { type: 'yaml' }, throws ERR_IMPORT_ATTRIBUTE_UNSUPPORTED with the message Import attribute "type" with value "yaml" is not supported. The TC39 proposal asks implementations to reject attributes they have not implemented rather than ignore them, and Node follows that.
What happened to assert { type: ‘json’ }?
If you learned this feature in 2021 or 2022, you learned it as assert { type: 'json' }. That keyword is gone.
The proposal’s own history explains the churn. It started in 2019 as “module attributes” using with. At Stage 3 in 2020 it became “import assertions” and the keyword changed to assert. In January 2023 the committee demoted it to Stage 2 over incompatibilities with the web platform, and two months later it came back to Stage 3 as “import attributes” with with restored. It is now Stage 4.
Runtimes followed. Node added with in 20.10.0 and 18.20.0 and dropped assert entirely in 22.0.0, so on Node 22 and later the old syntax is a plain SyntaxError: Unexpected identifier 'assert'. Chrome removed assert in 126. Firefox and Safari never shipped it. TypeScript 5.8 reports it as an error: Import assertions have been replaced by import attributes. Use 'with' instead of 'assert'.
A codebase that still has assert in it will not start on a current Node LTS. The fix is a search and replace, and nothing else about the import changes.
Why can’t I do a named import from JSON?
This one looks like it should work and does not:
import { version } from './package.json' with { type: 'json' };
// SyntaxError: The requested module './package.json' does not provide an export named 'version'
JSON modules expose a single default export and nothing else. Node’s docs say it directly (“There is no support for named exports”) and the spec agrees. Import the default and destructure it on the next line:
import pkg from './package.json' with { type: 'json' };
const { version, name } = pkg;
The confusion comes from bundlers. Vite supports import { field } from './example.json' because its json.namedExports option defaults to true and it rewrites the file into a module with one export per top-level key, which helps tree-shaking. That is a Vite transform, not JavaScript. Code written that way runs under Vite and breaks the moment the same file is executed by Node or a browser directly. If a module is shared between a Vite app and a Node script, use the default import in both.
Which runtimes and browsers support import attributes?
Everything current. According to MDN’s compat data (version 8.1.6 of @mdn/browser-compat-data), the with syntax and type: 'json' shipped in Chrome and Edge 123, Safari 17.2 on desktop and iOS, and Firefox 138. MDN marks import attributes as Baseline 2025, newly available since April 2025. If you are weighing how far to lean on native modules in the browser at all, our import maps versus bundler post covers that decision.
On the server side, Node unflagged JSON modules and import attributes in 22.12.0 and 20.18.3, with backports to 18.20.5. Deno has supported with since 1.37. Both import.meta.dirname and import.meta.filename went stable in Node 22.16 and 24.0, which matters if you build the JSON path yourself.
TypeScript needs two settings. Support for with arrived in TypeScript 5.3, and TypeScript does not check the attribute’s contents; it passes them through for the runtime to validate. The import still has to be typed, which is what resolveJsonModule does, generating a type from the file’s static shape. And module has to be a setting that emits the attribute. The TypeScript 5.8 message for a wrong module reads: Import attributes are only supported when the '--module' option is set to 'esnext', 'node18', 'nodenext', or 'preserve'.
{
"compilerOptions": {
"module": "nodenext",
"resolveJsonModule": true
}
}
With module set to commonjs, the import fails to compile even though resolveJsonModule is on, and because the error names the module option and not the JSON file, people go hunting in the wrong place. For TypeScript files run straight by Node, our write-up on type stripping covers what else module: "nodenext" changes.
When should you use require or readFile instead?
Three cases.
The first is a .cjs file or a package without "type": "module". There require('./data.json') has worked since Node existed, Node parses the file as JSON because of the extension, and no attribute is involved. Do not convert a CommonJS file to ESM just to use the new syntax.
The second is an ES module that wants require semantics, usually because a dependency’s path is computed at runtime or because the file sits behind a package.json exports map that only exposes it to require. createRequire from node:module gives you a working require scoped to the current file:
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const rates = require('./rates.json');
Node keeps one cache for both. The ESM docs state that a JSON module import creates an entry in the CommonJS cache, and that require of the same path returns the same object. So mixing the two does not cost you a second parse, but it does mean a mutation through one path is visible through the other.
The third case is data that changes while the process runs. Module imports are cached for the life of the process and there is no supported way to re-import a changed file. A config file that an operator edits in place, a locale bundle swapped by a deploy, anything you expect to re-read: use fs.readFile and JSON.parse, and own the caching yourself.
import { readFile } from 'node:fs/promises';
const raw = await readFile(new URL('./rates.json', import.meta.url), 'utf8');
const rates = JSON.parse(raw);
That version also lets you validate the shape before trusting it, which an import never will.
What I would do
Use with { type: 'json' } for anything that is genuinely part of the program: package.json for a version string, a locale bundle, a static lookup table shipped with the code. Target Node 20.18.3 or later so the feature is stable without flags, replace every assert while you are in there, and import the default rather than fighting for named exports.
Keep require where the file is already CommonJS. Reach for readFile when the data can change under you, or when you would want to validate it anyway. The import syntax is the right default; it is just not the right tool for a file you do not fully trust.