StackDevLife
Import Attributes in ES2025 — assert vs with and Why the Spec Changed
Back to Blog

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.

SB

Sandeep Bansod

January 12, 20267 min read
Share:

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:

JavaScript
// 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:

JavaScript
// 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.

JavaScript
// 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

Bash
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:

JavaScript
// 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():

JavaScript
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:

JavaScript
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:

JavaScript
// 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:

JavaScript
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

Bash
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:

TypeScript
// tsconfig.json
{
  "compilerOptions": {
    "module":           "NodeNext",
    "moduleResolution": "NodeNext",
    "target":           "ES2022"
  }
}
TypeScript
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 exist

Common 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.

SB

Sandeep Bansod

I'm a Front‑End Developer located in India focused on website look great, work fast and perform well with a seamless user experience. Over the years I worked across different areas of digital design, web development, email design, app UI/UX and developemnt.

Related Articles

You might also enjoy these

Stay in the loop

Get articles on technology, health, and lifestyle delivered to your inbox.No spam — unsubscribe anytime.