Import Attributes in ES2025 — assert vs with and Why the Spec Changed
You changed assert to with and everything worked again — but you have no idea why the syntax changed, whether it's safe to use, or what happens in environments that still expect assert. Here's the full story, the exact migration, and every edge case that will trip you up.
You updated your bundler. Your JSON imports stopped working. The error said something about assert being deprecated. You changed assert to with and everything worked again — but you have no idea why the syntax changed, whether it's safe to use, or what happens in the environments that still expect assert.
What import attributes are
Before import attributes existed, if you tried to import a JSON file, the browser and Node.js had no way to verify what type of content they were about to execute. A malicious CDN or a misconfigured server could serve JavaScript disguised as JSON, and your import data from './config.json' would execute it as code.
Import attributes solve this by letting you declare the expected type inline with the import:
// You declare what type of content you expect
import config from './config.json' with { type: 'json' };The runtime checks that the file's actual content type matches. If a server serves JavaScript where you declared JSON, the import fails — intentionally. The attribute is a security assertion about the content you're importing, not a hint.
The assert syntax — what you were writing
When the proposal first shipped in Chrome 91 and Node.js 17, the keyword was assert:
// The original syntax — now deprecated
import data from './data.json' assert { type: 'json' };
import styles from './styles.css' assert { type: 'css' };
import workers from './worker.wasm' assert { type: 'webassembly' };
// Dynamic imports too
const data = await import('./data.json', { assert: { type: 'json' } });This shipped in V8, SpiderMonkey, and Node.js before the TC39 proposal reached Stage 3. It was widely adopted — Vite, esbuild, dozens of tutorials. And then TC39 changed the keyword entirely.
Why the spec changed to with
The assert keyword was rejected at the TC39 standardisation stage for a precise semantic reason: it implies the attribute is purely a check — that removing it leaves behaviour the same and only disables safety enforcement. TC39 found this was not true.
The type attribute does not just validate content — it also changes how the module is parsed and evaluated. A JSON module is parsed as a data object, not executed as code. A CSS module is constructed as a CSSStyleSheet. Removing assert { type: 'json' } doesn't disable a safety check — it changes what the runtime does with the bytes. The keyword assert was semantically wrong.
with is semantically correct: import this, with the following attributes affecting how it's processed.
// ES2025 — the standardised syntax
import config from './config.json' with { type: 'json' };
import sheet from './theme.css' with { type: 'css' };
// Dynamic import — note the property name changes too
const config = await import('./config.json', { with: { type: 'json' } });Browser and runtime support right now
Chrome 123+ → with (native) assert (deprecated warning)
Firefox 125+ → with (native)
Safari 17.2+ → with (native)
Node.js 22+ → with (stable) assert (deprecated)
Node.js 20.10+ → with (experimental)
Bun 1.0+ → with (native)
Deno 1.37+ → with (native) assert (removed in 2.0)
Chrome 91–122 → assert only (no with support)
Node.js 17–20.9 → assert only (no with support)The practical consequence: if you're targeting Node.js 18 LTS specifically, assert is the only syntax that works. If you're targeting Node.js 22+, with works and assert logs a deprecation warning.
The exact migration
Static imports, dynamic imports, and re-exports all change:
// Static imports
// Before
import config from './config.json' assert { type: 'json' };
// After
import config from './config.json' with { type: 'json' };
// Dynamic imports — the property name change is easy to miss
// Before
const config = await import('./config.json', { assert: { type: 'json' } });
// After
const config = await import('./config.json', { with: { type: 'json' } });
// Re-exports
// Before
export { default as config } from './config.json' assert { type: 'json' };
// After
export { default as config } from './config.json' with { type: 'json' };Real usage — JSON, CSS, and WASM modules
JSON modules — the most common case. The runtime parses it; you get the object directly, no JSON.parse, no fs.readFileSync, no fetch and .json():
import packageInfo from './package.json' with { type: 'json' };
import i18n from './en-US.json' with { type: 'json' };
console.log(packageInfo.version); // '2.4.1' — no parsing needed
console.log(i18n.greeting); // 'Hello'
// Dynamic — useful when locale is determined at runtime
const locale = navigator.language;
const strings = await import(`./locales/${locale}.json`, {
with: { type: 'json' }
});CSS modules — gives you a CSSStyleSheet object for use with adoptedStyleSheets in documents and shadow roots:
import sheet from './button.css' with { type: 'css' };
// Apply to the document
document.adoptedStyleSheets = [sheet];
// Or scope to a Web Component shadow root
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}Using both syntaxes for cross-environment compatibility
If your code has to run in environments that only support assert and environments that only support with, the cleanest approach is a build-time transform via your bundler:
// vite.config.js — Vite 5.1+ handles this automatically
export default {
build: {
target: 'es2022' // adjusts import attribute syntax per target
}
};For manual handling in code that can't use a build tool, a dynamic import with try/catch works as a transition-window shim:
async function loadConfig() {
try {
// Try the standard syntax first
const mod = await import('./config.json', { with: { type: 'json' } });
return mod.default;
} catch {
// Fall back to assert for older Node.js / environments
const mod = await import('./config.json', { assert: { type: 'json' } });
return mod.default;
}
}Bundler and tooling state in 2026
Vite 5.1+ → with (native transform) assert (auto-upgraded)
Webpack 5.87+ → with (native) assert (deprecated warn)
esbuild 0.21+ → with (native)
Rollup 4.14+ → with (native)
TypeScript 5.3+ → with (type-checks both) assert (deprecation hint)
Babel 7.22+ → with (transform plugin)
Next.js 15+ → with (native via Turbopack)TypeScript handles the type side correctly since 5.3. With module: NodeNext in your tsconfig.json, JSON imports are fully typed from the file structure — no manual interface needed:
// tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022"
}
}import config from './config.json' with { type: 'json' };
// Fully typed from the JSON structure — no manual interface needed
console.log(config.database.host); // TypeScript knows this exists
console.log(config.database.port); // typed as number
console.log(config.database.xyz); // TypeScript error — property doesn't existCommon mistakes
- Changing assert to with in static imports but forgetting dynamic imports — static and dynamic imports are different syntax paths. Search your codebase for both assert: { type: and assert { — they look different and live in different places
- Expecting type: 'json' to work without bundler support — native JSON modules work in browsers and Node.js 22+, but older Node.js and unconfigured bundlers will reject them. Check your target before assuming it works
- Using with { type: 'json' } in Node.js 18 LTS — Node.js 18 only supports assert. If your production environment is Node.js 18, with is a syntax error. Know your runtime version before migrating
- Importing JSON with with but without type — import x from './file.json' with {} is valid syntax but the runtime may reject it or execute the content as JavaScript. Always include type: 'json'
- Treating import attributes as a bundler feature — they are a JavaScript language feature defined by the ECMAScript specification. Bundlers implement and sometimes transform the standard, but the semantics belong to the language, not the tool
- Confusing the dynamic import options key — import('./file.json', { with: { type: 'json' } }) — the outer property is with, not attributes or options. It mirrors the static syntax keyword exactly
The takeaway
The assert to with rename was not a breaking change for the sake of it. assert described the wrong behaviour — it implied a passive check when the attribute actively changes how modules are loaded and parsed. with is accurate. For new projects targeting Node.js 22+ or modern browsers, use with everywhere and never write assert. For projects still supporting Node.js 18 LTS, stick with assert until you can upgrade. For everything else, let Vite or your bundler handle the transform and set your minimum target explicitly.
Related Articles
You might also enjoy these
Array.fromAsync() and the End of Promise.all Map Patterns
Every JavaScript developer has written await Promise.all(items.map(async item =>...)). It works — until you hit a rate-limited API, a paginated async generator, or a ReadableStream. Array.fromAsync() is the purpose-built replacement you didn't know you needed.
Angular Signals Forms — Replace ReactiveFormsModule in New Projects
Reactive forms were the right solution for 2018. Angular 21 ships Signal-based Forms — no valueChanges, no async pipe, no subscription management. Here's how to replace ReactiveFormsModule in every new component you write.
Stay in the loop
Get articles on technology, health, and lifestyle delivered to your inbox.
No spam — unsubscribe anytime.