ECMAScript proposal: JSON modules

[2021-06-16] dev, javascript, es proposal
(Ad, please don’t block)

In this blog post, we examine the ECMAScript proposal “JSON modules” (by Sven Sauleau, Daniel Ehrenberg, Myles Borins, and Dan Clark). It lets us import JSON data as if it were an ECMAScript module.

Why would we want to import JSON like a module?  

Various bundlers (such as webpack) have allowed us to import JSON data as if it were an ECMAScript module for a long time. JSON modules turn this into a standard feature.

Why is that interesting? It provides a convenient way of using, e.g., configuration data in our apps. Take, for example, the following file structure:

my-app/
  src/
    config-data.json
    main.mjs

my-app/src/config-data.json looks as follows:

{
  "appName": "My App"
}

This is my-app/src/main.mjs:

import configData from './config-data.json' assert {type: 'json'};
console.log(`I am ${configData.appName}!`);

The syntax from assert until the end is called an import assertion. JSON modules were one of the use cases for which import assertions were created.

The default export of a JSON module contains the JSON data. There are no named exports.

Getting JSON data via fetch()  

Without JSON modules, we would have to use fetch():

async function fetchConfigData(relativePath) {
  const urlOfConfigData = new URL(
    relativePath, import.meta.url); // (A)
  const response = await fetch(urlOfConfigData.toString()); // (B)
  const json = await response.json(); // (C)
  return json;
}

const configData = await fetchConfigData('config-data.json');
console.log(`I am ${configData.appName}!`);

We are using two relatively new features:

fetch() has two downsides compared to JSON modules:

  • The code is slightly more complicated.
  • Node.js currently has no built-in support for fetch(). (And I suspect JSON modules will be supported sooner than fetch().)

Dynamically importing JSON modules via import()  

The previous import statement was static (fixed at runtime). We can also import JSON modules dynamically (changeably at runtime):

async function importConfigData(moduleSpec) {
  const namespaceObj = await import(  // (A)
    moduleSpec, {assert: {type: 'json'}});
  return namespaceObj.default; // (B)
}

const configData = await importConfigData('./config-data.json');
console.log(`I am ${configData.appName}!`);

Note that the import() operator (line A) returns a module namespace object. That is why we return the value of property .default (which contains the default export) in line B.

Why the extra syntax?  

You may wonder why we have to use extra syntax at the end of the important statement:

import configData from './config-data.json' assert {type: 'json'};

Why can’t JavaScript detect that this is JSON by looking at the filename extension?

import configData from './config-data.json';

This is not possible because it can cause security issues: Browsers never look at filename extensions, they look at content types. And servers are responsible for providing content types for files. Therefore, two things can happen when importing a .json file:

  • Our own server might send a content type other than application/json and the importing would go wrong in some manner.
  • With an external server, things are even more risky: If it sends the file with the content type text/javascript, it could execute code inside our app.

Therefore, JavaScript won’t rely on content types when importing JSON.

Availability  

More information on modules in JavaScript