Node.js

JSONの型定義とデータ型一覧|6つの型とTypeScriptで型安全に扱う方法

JSONで使えるデータ型は、文字列・数値・真偽値・オブジェクト・配列・nullの6つだけです。日付型も整数型も存在しません。この「型が少ない」という仕様が、日付が文字列に化ける、大きなIDの下2桁が変わる、JSON.parse()の戻り値がanyになって型チェックをすり抜ける、といった実務の事故に直結します。JSONの型の一覧と、TypeScriptで型定義して安全に受け取るまでを、動くコードで整理します。

まとめ

  • JSONの型は6つ(string / number / boolean / object / array / null)。RFC 8259とECMA-404が規定する仕様で、これ以上増えません。
  • Date・undefined・NaN・Infinity・BigInt・関数・コメントはJSONに存在しないJSON.stringify()は黙って落とすかnullに変え、BigIntだけはTypeErrorで落ちます。
  • 数値はIEEE 754倍精度Number.MAX_SAFE_INTEGER(9007199254740991)を超えるIDは丸められます。防ぐにはIDを文字列で持つか、ES2026のJSON.rawJSON()とreviverの第3引数context.sourceを使います。
  • TypeScriptでの型定義はinterfaceか型エイリアスで書くが、それだけでは安全にならないJSON.parse()の戻り値の型はanyで、型注釈は実行時に何も検証しないためです。Zodなどでランタイム検証して初めて型が保証されます。
  • 「JSON型」はデータベースのカラム型を指すこともある。MySQL 8.0のJSON型、PostgreSQLのjsonbはバイナリ格納・インデックス可で、テキストとして保存するのとは別物です。

JSONで使える6つのデータ型と、使えない型

JSONはRFC 8259(IETF、2017年12月発行・STD 90)とECMA-404(第2版、2017年12月)で仕様が固定されています。両者は同じ文法を規定しており、値として取れる型は次の6つに限られます。

6つの型の一覧と書式

JSONでの書式 JavaScriptでの型 注意点
文字列(string) "hello" string ダブルクォート必須
数値(number) 42 / -1.5 / 1e3 number 整数と小数の区別なし
真偽値(boolean) true / false boolean 小文字のみ
オブジェクト(object) {"id": 1} object キーは文字列のみ
配列(array) [1, 2, 3] Array 型の混在も可
null null object(typeofの罠) 「値が無い」の唯一の表現

数値に整数型がない点は、他言語からJSONに移ってきた開発者がつまずきやすい箇所です。JSONの11.0は同じ値で、パース後はどちらもJavaScriptのnumberになります。intとfloatを区別したいなら、JSON側では表現できないためAPIの仕様書で保証するか、文字列で送るしかありません。真偽値はtruefalseのみで、True1は使えません。文字列をシングルクォートで囲むのも構文エラーです。

nullには別の落とし穴があります。typeof nullはJavaScriptの初期からの仕様上の欠陥で"object"を返すため、typeof value === "object"ではnullを弾けません。null判定はvalue === nullで書きます。実行時の型判定の詳細はtypeof演算子の使い方|JavaScriptの型判定とTypeScriptのtypeof型・keyof型にまとめています。

JSONに存在しない型と、stringifyの変換結果

JavaScriptの値をそのままJSON.stringify()に渡すと、JSONに対応する型がない値は消えるか別の値に変換されます。エラーで気づけるのはBigIntだけで、他は無言で欠落するため、バグの発見が遅れます。

JavaScriptの値 JSON.stringify()の結果
Date ISO 8601形式の文字列
undefined(プロパティ) キーごと省略
undefined(配列の要素) null
関数・Symbol undefinedと同じ扱い
NaN・Infinity null
BigInt TypeError

Dateが文字列になるのは、Date.prototype.toJSON()が呼ばれるためです。

const data = {
  createdAt: new Date("2026-07-12T09:00:00Z"),
  memo: undefined,
  score: NaN,
  tags: ["a", undefined, "c"],
};

console.log(JSON.stringify(data));
// {"createdAt":"2026-07-12T09:00:00.000Z","score":null,"tags":["a",null,"c"]}
// memo は消え、NaN と undefined の要素は null になる

JSON.stringify({ id: 10n });
// TypeError: Do not know how to serialize a BigInt

Dateは往復しません。stringifyで文字列になり、parseで文字列のまま戻ります。日付として使いたい側でnew Date(値)に戻す責任があり、これを忘れてdate.getTime is not a functionで落ちるのは頻出パターンです。reviver(後述)でパース時にまとめて復元するのが定石です。

JSONの基本構造|キー・値・オブジェクト・配列

JSONは「キーと値のペア」をオブジェクト{}で、値の並びを配列[]でまとめます。両者は入れ子にでき、APIレスポンスの多くは「オブジェクトの配列」の形をとります。

{
  "userId": 1024,
  "name": "山田太郎",
  "isActive": true,
  "lastLogin": null,
  "profile": {
    "department": "開発部",
    "skills": ["TypeScript", "Go"]
  },
  "orders": [
    { "orderId": "A-001", "amount": 12800 },
    { "orderId": "A-002", "amount": 4300 }
  ]
}

キーは必ずダブルクォートで囲んだ文字列です。JavaScriptのオブジェクトリテラルではクォートを省略できますが、JSONでは省略した時点で構文エラーになります。「JavaScriptのオブジェクトをそのまま貼れば動く」という思い込みが、設定ファイルが読めない原因になりがちです。

JSONの構文エラー4パターンとエスケープ

誤り 正しい書き方
末尾カンマ {"a": 1,} {"a": 1}
シングルクォート {'a': 1} {"a": 1}
キーのクォート省略 {a: 1} {"a": 1}
コメント {"a": 1} // メモ コメントは書けない

文字列に含める特殊文字はバックスラッシュでエスケープします。ダブルクォートは\"、バックスラッシュは\\、改行は\n、タブは\tです。生の改行を文字列リテラルの中に入れるとエラーになるため、複数行テキストは\nで表現します。Windowsのファイルパスを値に入れるときは"C:\\Users\\app"のように二重にする必要があります。文字コードはRFC 8259でUTF-8が既定と定められており、BOM付きのファイルはパーサによって弾かれます。

TypeScriptでのJSON型定義|interfaceとランタイム検証の分業

ここが「json 型定義」で検索したときの本題です。TypeScriptではJSONの構造をinterfaceまたは型エイリアスで宣言しますが、宣言しただけでは実行時の安全は得られません。型定義(コンパイル時)と検証(実行時)を別物として設計するのが要点です。

interfaceと型エイリアスによる構造宣言|「?」と「| null」の違い

JSONの構造とTypeScriptの型は素直に対応します。値が来ないことがあるキーは?(省略可能)、nullが来るキーは| nullで表現します。この2つは意味が違い、混同するとnullチェック漏れになります。

interface Order {
  orderId: string;
  amount: number;
}

interface User {
  userId: number;
  name: string;
  isActive: boolean;
  lastLogin: string | null;   // キーは必ず存在するが null が入りうる
  nickname?: string;          // キー自体が無いことがある
  profile: {
    department: string;
    skills: string[];
  };
  orders: Order[];
}

JSONの型は6つしかないため、TypeScript側で表現力を上げたいなら型定義の工夫が要ります。たとえばユーザーIDと注文IDはどちらもJSON上はただの文字列ですが、取り違えるとバグになります。両者を別の型として扱うテクニックはBranded Typesとは?型の安全性を向上させる技術の概要で解説しています。

JSON.parse()がanyを返す問題|型注釈がすり抜ける理由

TypeScriptの標準ライブラリ(lib.es5.d.ts)でJSON.parse()の戻り値はanyと宣言されています。つまり、下のコードは型チェックを通りますが、実行時にはAPIが何を返してきても素通りします。

const user: User = JSON.parse(text);
// 型エラーにはならない。any は何にでも代入できるため。
// text が {"foo": 1} でも通り、user.name は undefined になる。

console.log(user.name.toUpperCase());
// TypeError: Cannot read properties of undefined (reading 'toUpperCase')

型注釈もas Userによるアサーションも、コンパイル後には消えます。「TypeScriptで書いたから型は安全」は、外部から入ってくるJSONについては成立しません。

最初の一歩として、JSON.parse()の戻り値をunknownで受ける書き方を勧めます。anyと違いunknownは絞り込むまで使えないため、検証を書き忘れた箇所がコンパイルエラーとして表面化します。

const parsed: unknown = JSON.parse(text);
// parsed.name はここではコンパイルエラー。検証を通すまで触れない。

Zod 4によるランタイム検証|z.inferによる型定義の一元化

実行時に構造を検証し、成功したときだけ型付きの値を返すのがスキーマバリデーションです。TypeScript向けではZodが定番で、2026年7月時点の最新は4系(4.4.3)です。スキーマからz.inferで型を導出できるため、interfaceとスキーマを二重に書く必要がなくなります。

import { z } from "zod";

const OrderSchema = z.object({
  orderId: z.string(),
  amount: z.number(),
});

const UserSchema = z.object({
  userId: z.number(),
  name: z.string(),
  isActive: z.boolean(),
  lastLogin: z.string().nullable(),
  nickname: z.string().optional(),
  profile: z.object({
    department: z.string(),
    skills: z.array(z.string()),
  }),
  orders: z.array(OrderSchema),
});

type User = z.infer<typeof UserSchema>;   // スキーマから型を生成(interface不要)

function loadUser(text: string): User {
  const result = UserSchema.safeParse(JSON.parse(text));
  if (!result.success) {
    console.error(result.error.issues);    // どのキーがどう違うかが出る
    throw new Error("APIレスポンスの形式が想定と違います");
  }
  return result.data;                      // ここでは User 型が保証されている
}

導入手順とエラーメッセージの扱いはZodの使い方入門|npm・yarn・pnpmでのインストールとTypeScript型安全バリデーションにまとめています。バンドルサイズを削りたい場合はZod Miniや、より軽量なValibotという選択肢もあり、比較はValibotとは?Zodとの違いとpipe構文の使い方をv1系の実コードで解説で扱っています。

ただし、すべてのJSONにスキーマを書くべきではありません。自社内で生成した設定ファイルや、ビルド時に固定される定数のように、入力が外部から変わりえないJSONにバリデーションを足しても、実行時コストと保守対象が増えるだけです。検証を入れる価値があるのは、外部API・ユーザー入力・別チームが管理するファイルなど「壊れた形で来る可能性がある」JSONに限られます。

resolveJsonModuleでの.jsonインポートと型推論

プロジェクト内の静的なJSONファイルは、tsconfig.jsonでresolveJsonModuleを有効にすればimportでき、TypeScriptがファイルの中身から型を推論します。外部入力ではないため、この経路にバリデーションは不要です。

{
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true
  }
}

config.json{ "apiBase": "https://example.com", "retry": 3 }なら、retryはnumberとして推論されます。

import config from "./config.json";

config.retry.toFixed();     // number として推論される
config.apiBase.trim();      // string として推論される

Node.jsのESM("type": "module")でJSONをインポートする場合は、import attributes構文with { type: "json" }が必須です。TypeScriptはこの構文を5.3でサポートしました。CommonJSやバンドラ経由のビルドでは不要なため、実行環境がESMかどうかで書き分けます。

import config from "./config.json" with { type: "json" };

JSON.parse()とJSON.stringify()の実務的な使い方

変換の基本とエラーハンドリング

JSON.parse()は文字列をJavaScriptの値に、JSON.stringify()は値をJSON文字列に変換します。パースは不正な入力でSyntaxErrorを投げるため、外部から受け取る文字列は必ずtry-catchで囲みます。戻り値をnullで表すと呼び出し側がチェックを忘れるので、成否をタグ付きの型で返します。

type ParseResult =
  | { ok: true; value: unknown }
  | { ok: false; message: string };

function tryParseJson(text: string): ParseResult {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (e) {
    if (e instanceof SyntaxError) {
      return { ok: false, message: e.message };
    }
    throw e;
  }
}

function handle(text: string): void {
  const res = tryParseJson(text);
  if (!res.ok) {
    console.error("JSONの構文が不正です:", res.message);
    return;
  }
  console.log(res.value);   // ok:true に絞り込んで初めて value に触れる
}

空文字列""もSyntaxErrorになります。

fetchのres.json()とJSON.parse()の使い分け|204と空ボディの扱い

ブラウザやNode.jsからHTTPでJSONを読み込むときは、JSON.parse()を直接呼ばずres.json()を使うのが一般的です。ただしres.json()は内部でパースするだけなので、ボディが空だと同じくSyntaxError(Unexpected end of JSON input)で落ちます。ステータス204(No Content)やContent-Lengthが0のレスポンスは、パースの前に弾きます。

async function fetchUser(id: number): Promise<User | null> {
  const res = await fetch(`https://example.com/api/users/${id}`);

  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  if (res.status === 204) {
    return null;            // ボディが無いので res.json() は呼ばない
  }

  const data: unknown = await res.json();   // 戻り値の型は Promise<any>
  return UserSchema.parse(data);            // ここで初めて型が確定する
}

res.json()の戻り値も型定義上はPromise<any>です。JSON.parse()と同じ落とし穴があるため、unknownで受けてスキーマ検証を通す流れは変わりません。なお、リクエスト側でJSONを送るかフォーム形式で送るかの判断はapplication/x-www-form-urlencoded と application/json の違い|使い分けと変換方法を解説で整理しています。

replacer・space・reviverの使い分け|機密のマスクと日付の復元

JSON.stringify(value, replacer, space)の第2引数replacerは、出力から特定のキーを除外したり値を変換したりする関数(またはキー名の配列)です。パスワードやトークンをログに残さない用途で使います。第3引数spaceはインデント幅で、ログや設定ファイルの可読性を上げます。

const user = { id: 1, name: "山田", password: "secret", token: "abc123" };

const masked = JSON.stringify(user, ["id", "name"], 2);
// {
//   "id": 1,
//   "name": "山田"
// }

const replaced = JSON.stringify(user, (key, value) =>
  key === "password" || key === "token" ? undefined : value
);
// {"id":1,"name":"山田"}

対になるのがJSON.parse(text, reviver)のreviverで、パース中の各値を書き換えられます。ISO 8601形式の文字列をDateに戻す処理をここに集約すれば、呼び出し側で毎回new Date()を書かずに済みます。

const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;

const data = JSON.parse(text, (key, value) =>
  typeof value === "string" && ISO.test(value) ? new Date(value) : value
);
// createdAt が Date インスタンスとして復元される

大きな数値の精度落ちを防ぐJSON.rawJSON()とreviverのsource

JSONの数値はIEEE 754倍精度浮動小数点数で扱われます。JavaScriptが正確に表せる整数の上限はNumber.MAX_SAFE_INTEGER(9007199254740991)で、これを超えるID・金額・タイムスタンプは、パースした時点で末尾が変わります。

JSON.parse('{"id": 12345678901234567890}').id;
// 12345678901234567000  ← 下位の桁が失われている

JSON.stringify({ id: 12345678901234567890 });
// {"id":12345678901234567000}

SnowflakeIDのような64bit整数をJSONで受け渡すシステムでは、これが実害を出します。実務での第一選択はAPIの設計段階でIDを文字列型にすることです。X(旧Twitter)のAPIがidid_strを併記しているのは、この問題への対処です。

設計を変えられない場合、ECMAScript 2026で標準化された「JSON.parse source text access」が使えます。JSON.rawJSON()で数値を文字列表現のまま埋め込み、reviverの第3引数context.sourceで元のテキストを受け取ることで、numberを経由せずに値を運べます。MDNではBaseline 2025(2025年3月から主要ブラウザで利用可)として扱われています。

// パース: 元のテキストを BigInt にそのまま渡す
const parsed = JSON.parse('{"id": 12345678901234567890}', (key, value, context) => {
  if (key === "id") {
    return BigInt(context.source);   // 桁を失わない
  }
  return value;
});
console.log(parsed.id);   // 12345678901234567890n

// シリアライズ: rawJSON でクォート無しの数値として書き戻す
const text = JSON.stringify({ id: JSON.rawJSON(parsed.id.toString()) });
console.log(text);        // {"id":12345678901234567890}

古いブラウザやNode.jsを対象に含むなら、core-jsのポリフィルを入れるか、IDを文字列で運ぶ設計に倒してください。JSON.rawJSON()に渡せるのはプリミティブ値のJSONテキストのみで、オブジェクトや配列を渡すとSyntaxErrorになります。

データベースの「JSON型」(MySQL・PostgreSQL)

「JSON型」という言葉は、データ形式としてのJSONの型だけでなく、RDBのカラム型を指すことがあります。検索結果にMySQLやPostgreSQLのマニュアルが混じるのはこのためです。文字列カラムにJSONを突っ込むのとは、以下の点で別物です。

項目 MySQL 8.0 JSON型 PostgreSQL json PostgreSQL jsonb
格納形式 バイナリ テキストのまま バイナリ
挿入時の検証 あり あり あり
キー順・空白の保持 しない する しない
インデックス 生成列+索引/8.0.17でマルチバリュー GIN不可(式インデックスのみ) GINインデックス可
-- MySQL 8.0: JSON型カラムと値の取り出し
CREATE TABLE mysql_events (id INT PRIMARY KEY, payload JSON);
SELECT payload->>'$.userId' AS user_id FROM mysql_events;
-- PostgreSQL: jsonb と GIN インデックス
CREATE TABLE pg_events (id INT PRIMARY KEY, payload JSONB);
CREATE INDEX idx_payload ON pg_events USING GIN (payload);
SELECT payload->>'userId' AS user_id FROM pg_events WHERE payload @> '{"type":"login"}';

検索条件に使うキーは、JSONに押し込まず通常のカラムに切り出すべきです。JSON型のカラムは「スキーマが定まらない付加情報」を保持するための逃げ道であって、正規化の代替ではありません。抽出頻度の高いキーをJSONの中に埋めたままインデックスを張り続ける設計は、テーブルが育つほど扱いにくくなります。

JSONとXML・CSV・YAMLの違い

形式 構造 コメント 主な用途
JSON 階層あり 不可 Web API・設定ファイル
XML 階層あり(属性・名前空間) SOAP・文書構造・レガシー連携
CSV 表形式のみ 不可 表データの一括入出力
YAML 階層あり CI設定・Kubernetesマニフェスト

YAMLはJSONのスーパーセット(YAML 1.2)で、有効なJSONはそのままYAMLとして読めます。人が手で書き換える設定ファイルにJSONを選ぶべきではありません。コメントが書けず末尾カンマも許されないため、編集ミスが構文エラーに直結するからです。人が書く設定はYAML、表形式の一括入出力はCSV、機械同士のデータ交換は曖昧さの少ないJSON、という住み分けが実態に合っています。HTMLフォームからの送信形式との違いはapplication/x-www-form-urlencoded と application/json の違い|使い分けと変換方法を解説を参照してください。

よくある質問(FAQ)

JSON型とは何ですか?

2つの意味があります。1つはJSONの値として使えるデータ型(string・number・boolean・object・array・nullの6種)。もう1つはMySQLやPostgreSQLでJSONを格納するカラム型です。文脈で判断してください。

JSONで日付はどう扱いますか?

JSONに日付型はないため、ISO 8601形式の文字列("2026-07-12T09:00:00.000Z")で持つのが標準的です。JSON.stringify()はDateを自動でこの形式にしますが、JSON.parse()は文字列のまま返します。Dateに戻すにはreviverか受け取り側での変換が必要です。

JSONにコメントは書けますか?

書けません。RFC 8259にコメントの規定がなく、//を含むJSONはパースエラーになります。設定ファイルでコメントを残したい場合は、JSONC(VS Codeのsettings.jsonなどが採用)やYAMLを使うか、"_comment"のようなキーで代用します。

JSONの数値に整数型はありますか?

ありません。11.0もJSONでは同じnumberで、パース後はJavaScriptのnumberになります。整数であることを保証したい場合は、受け取り側でNumber.isInteger()やスキーマバリデーション(Zodのz.number().int()など)で検証します。

JSONの読み方と拡張子は?

「ジェイソン」と読みます。JavaScript Object Notationの略で、ファイル拡張子は.json、HTTPのMIMEタイプはapplication/jsonです。文字コードはRFC 8259でUTF-8が既定と定められています。

関連記事

資料請求

RELATED POSTS 関連記事