How to Safely Load JSON Modules in JavaScript

Import assertions and their updated standard, import attributes, provide a secure mechanism for JavaScript applications to load external data formats like JSON directly through native ES module syntax. This article explores the critical security risks associated with importing data modules, the architectural evolution from assertions to attributes, and how explicitly declaring module types prevents cross-site script execution and MIME-type confusion attacks in modern web environments.

The Security Vulnerability of Direct Module Imports

Before the introduction of import assertions, importing non-JavaScript resources natively using the ES module import statement posed a severe security concern known as module type confusion. In standard web architecture, a browser relies on the server's Content-Type header (such as application/json or text/javascript) to determine how a resource should be executed.

If a client requested a JSON resource using standard syntax:

import config from './data.json';

A compromised or malicious server could respond with a MIME type of text/javascript containing executable code instead of raw data. The browser would parse and execute this payload with full privileges in the user's execution context, leading to Cross-Site Scripting (XSS) or privilege escalation.

How Import Attributes Solve the Problem

Import attributes introduce an explicit, client-side declaration of the expected module type. By appending metadata to the import declaration, developers instruct the runtime to validate the resource type strictly before execution.

If the incoming resource does not strictly adhere to the expected format and MIME type, the module graph instantiation fails immediately with a fatal error. The payload is never parsed as executable code, completely mitigating the risk of dynamic script injection through spoofed responses.

Syntax: Evolution from Assertions to Attributes

The feature was originally proposed as "Import Assertions" using the assert keyword. However, the TC39 committee refined the specification into "Import Attributes" using the with keyword to allow attributes to influence module loading behaviors beyond simple runtime verification.

Static Import Syntax

To import a JSON module statically, append the with clause defining the module type:

import userSettings from './settings.json' with { type: 'json' };

console.log(userSettings.theme);

Dynamic Import Syntax

Dynamic imports (import()) support import attributes through a second configuration argument:

async function loadUserData(userId) {
  const data = await import(`./users/${userId}.json`, {
    with: { type: 'json' }
  });
  return data.default;
}

Key Safety Guarantees

  1. Strict Parsing Rules: When { type: 'json' } is specified, the resource is parsed strictly according to ECMAScript's JSON.parse grammar. Any syntax outside the JSON standard results in a syntax error rather than executable fallback code.
  2. Immutable Identity: The module exports a default export representing the parsed JSON object. The object behaves as standard module data, ensuring consistency throughout the module cache.
  3. MIME Type Enforcement: Browsers enforce a strict match against standard JSON MIME types (such as application/json). If a server delivers the resource with an incompatible MIME type, the import fails before evaluation occurs.

Runtime and Browser Support

Modern JavaScript runtimes, including Chromium-based browsers, Safari, Node.js, and Deno, natively support the with syntax for JSON modules. Legacy implementations supporting assert are being phased out in favor of the standardized with attribute syntax, providing a reliable and safe pathway to manage static configuration files, localization dictionaries, and data schemas across client and server environments.