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の1と1.0は同じ値で、パース後はどちらもJavaScriptのnumberになります。intとfloatを区別したいなら、JSON側では表現できないためAPIの仕様書で保証するか、文字列で送るしかありません。真偽値はtrue/falseのみで、Trueや1は使えません。文字列をシングルクォートで囲むのも構文エラーです。
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がidとid_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の数値に整数型はありますか?
ありません。1も1.0もJSONでは同じnumberで、パース後はJavaScriptのnumberになります。整数であることを保証したい場合は、受け取り側でNumber.isInteger()やスキーマバリデーション(Zodのz.number().int()など)で検証します。
JSONの読み方と拡張子は?
「ジェイソン」と読みます。JavaScript Object Notationの略で、ファイル拡張子は.json、HTTPのMIMEタイプはapplication/jsonです。文字コードはRFC 8259でUTF-8が既定と定められています。