ESM("type": "module" や .mjs)で JSON をそのまま import すると、次のエラーで止まることがあります。
node:internal/modules/esm/assert:88
throw new ERR_IMPORT_ATTRIBUTE_MISSING(url, 'type', validType);
^
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///app/data.json" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
終了コードは 1 です。Node 18・20 では、同じ状況でエラー名が ERR_IMPORT_ASSERTION_TYPE_MISSING になります(後述)。
原因は、ESM で JSON を import するには with { type: "json" } という import attribute(型属性)が必須で、それを書いていないことです。属性なしの import data from "./data.json" は、CommonJS の require("./data.json") やバンドラ経由なら通っていた書き方ですが、Node の素の ESM では通りません。直し方は、import 文に with { type: "json" } を足すことです。
// 旧:属性なし(ESM では ERR_IMPORT_ATTRIBUTE_MISSING)
// import data from "./data.json";
// 新:type: json の import attribute を付ける
import data from "./data.json" with { type: "json" };
console.log(data.answer);
なぜ起きるのか:ESM は JSON を「型を明示して」読む
JavaScript のモジュール仕様は、JSON や今後の別形式を import するとき、何として解釈するかを import 側が明示する仕組み(import attributes)を導入しました。with { type: "json" } が「このモジュールは JSON として読む」という指定です。Node の ESM ローダーは、JSON を読むときこの属性を要求し、無ければモジュールを読み込む前(validateAttributes の段階)で ERR_IMPORT_ATTRIBUTE_MISSING を投げます。
エラーがコード本体の実行ではなく読み込み時に出るのは、import の解決がモジュール評価より前に走るためです。console.log などの行までは到達していません。
なぜ ESM でだけ出るのか
境界は モジュールの種類(CommonJS か ESM か)とツールチェーンで、Node の版ではありません。同じ JSON 読み込みでも、どこで実行するかで属性の要否が変わります。
- CommonJS から ESM へ移した:CommonJS の
require("./data.json")は属性なしで JSON を読めます。"type": "module"を付けた、あるいは.mjsにした瞬間、同じ import にwith { type: "json" }が要るようになります。ESM 化にともなって表面化する型の一つです。 - バンドラ(webpack / Vite など)では書かずに通っていた:バンドラは属性なしの JSON import を独自に解決します。同じコードを Node で直接動かす(テスト・SSR・スクリプト実行など)と、素の ESM ローダーが属性を要求します。「ビルドは通るのに
nodeで実行すると落ちる」形になります。
Node 18・20 でも属性なしは同じくエラー(終了コード 1)ですが、エラー名が ERR_IMPORT_ASSERTION_TYPE_MISSING です。これは属性の仕様が assert から with に置き換わった名残で、名前が違うだけで対処は同じ(属性を書き足す)です。Node 22 以降は ERR_IMPORT_ATTRIBUTE_MISSING に変わりました。
直し方:with { type: “json” } を書き足す
static import なら、import 文の末尾に with { type: "json" } を付けます。
import data from "./data.json" with { type: "json" };
dynamic import なら、第2引数のオプションに with を渡します。
const { default: data } = await import("./data.json", { with: { type: "json" } });
with 構文が使えるのは Node 18.20 / 20.10 / 21 以降です。それより古い版では with 自体が構文エラー(SyntaxError: Unexpected token 'with')になるので、そのときは JSON import をやめて fs で読むのが版に依存しない方法です。
import { readFileSync } from "node:fs";
const data = JSON.parse(readFileSync(new URL("./data.json", import.meta.url), "utf8"));
切り分け(うまくいかないとき)
- エラー名が
assertを含む(SyntaxError: Unexpected identifier 'assert'):それは属性を書いていないのではなく、古いassert構文で書いているケースです。assert { type: "json" }をwith { type: "json" }に直します。詳しくは JSON import の assert を with に直す を参照してください。本記事は属性が「無い」ケース、あちらは属性を「古い書き方で書いている」ケースです。 with { type: "json" }を足したらExperimentalWarning: Importing JSON modules is an experimental featureが出る:境界に近い版(18.20.0・20.10.0 など)では、正しく書いても実験的機能の警告が出ます。警告まで消えるのは、系統ごとに 18.20.5+ / 20.18.3+ / 22.12.0+ / 23.1.0+ です(「22.12 以降」とひとまとめに読むと、23.0.x で警告が消えていると誤解します。23 系は 23.1.0 からです)。エラーではないので動作には影響しませんが、stderr を厳しく見る CI では拾われます。- TypeScript で書いている:
with構文に対応するのは TypeScript 5.3 以降です。5.3 未満のtscはwithを構文として受け付けず、コンパイル段階でエラーになります(withをassertに変換するわけではありません)。その場合は TypeScript を更新します。逆に、入力を古いassertで書いていると、TypeScript はそれをそのまま出力に残すので、出た JS を Node 22 以降で実行すると落ちます(入力をwithに直します)。resolveJsonModuleは tsc の型解決の設定で、実行時に Node が求める属性とは別なので、resolveJsonModule: trueでもこのエラーは出ます。 - 同じ ESM 化で別のエラーも一緒に出ている:
"type": "module"にした途端に require is not defined in ES module scope や、相対 import の拡張子・ディレクトリ関連(ERR_MODULE_NOT_FOUND / ERR_UNSUPPORTED_DIR_IMPORT)も出ることがあります。原因はどれも同じ(ESM 扱いになったこと)です。
検証環境
node:22.23.1-alpine、ネットワーク不要- 再現:
"type": "module"の下でimport data from "./data.json";(属性なし)を実行すると、TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "..." needs an import attribute of "type: json"で終了コード 1 - 修正:同じ import に
with { type: "json" }を足すと、終了コード 0・シグネチャ消滅(42を出力)
再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差は import 文の with { type: "json" } の有無だけです。エラー名が Node 18.20.8 / 20.20.2 では ERR_IMPORT_ASSERTION_TYPE_MISSING、22.23.1 / 24.18.0 では ERR_IMPORT_ATTRIBUTE_MISSING になること(いずれも属性なしは終了コード 1)は、各版のイメージで実行して確認し、verification/probes.txt に記録しています。