JavaScriptの基礎 - JSON
概要
JSON (JavaScript Object Notation) は、JavaScriptのオブジェクトリテラル構文を基にした軽量なテキスト形式のデータ交換フォーマットである。
言語非依存の仕様として設計されており、JavaScriptに限らず多くのプログラミング言語でデータの送受信に広く使用されている。
JavaScriptでJSONを操作するための組み込みオブジェクト JSON は、2つのメソッドを提供する。
JSON.parse- JSON文字列をJavaScriptの値 (オブジェクト、配列、プリミティブ) に変換する。
JSON.stringify- JavaScriptの値をJSON文字列に変換 (シリアライズ) する。
シリアライズ時には、JavaScriptの全ての型がJSONとして表現できるわけではないことに注意が必要である。
undefined、関数、Symbol はオブジェクトプロパティから削除され、NaN や Infinity は null に変換される。
また、Date オブジェクトはISO 8601形式の文字列に変換されるが、JSON.parse では自動的に Date に戻されない。
JSONを利用したディープコピー (JSON.parse(JSON.stringify())) は手軽な手法であるが、上記の型の制約がある。
現代のWebブラウザやNode.js環境では、Date、Map、Set、循環参照を正しく処理できる structuredClone() が利用できる。
Tauri v2では、invoke() によるIPC通信でフロントエンドとRustバックエンド間のデータがJSONを介して自動的にシリアライズ・デシリアライズされる。
Rust側では serde クレートにより、構造体のフィールド名変換 (snake_case / camelCase) も自動化される。
JSONの基本
JSONの構文規則
JSONは厳格な構文規則を持つテキストフォーマットである。
JavaScriptのオブジェクトリテラルとは異なる点があるため、注意が必要である。
主な構文規則を以下に示す。
| 規則 | 説明 |
|---|---|
| キー名は必ず二重引用符 (") で囲む |
|
| 文字列値は二重引用符で囲む | "hello" は有効'hello' は無効
|
| 末尾カンマは使用不可 | 配列やオブジェクトの最後の要素の後にカンマがあると SyntaxError が発生する。
|
| コメントは使用不可 | JavaScript のような // コメント や /* コメント */ は無効
|
| 数値は整数または浮動小数点数のみ | Infinity および NaN はJSONとして不正な値である。
|
有効なJSONの例を以下に示す。
{
"name": "Alice",
"age": 30,
"isActive": true,
"address": null,
"scores": [85, 92, 78],
"profile": {
"city": "Tokyo",
"country": "Japan"
}
}
JSONの型 と JavaScript型の対応
JSONが扱える型は6種類であり、JavaScriptの全ての型とは対応していない。
| JSON型 | JavaScript型 | 備考 |
|---|---|---|
| string | string | 1対1対応 |
| number | number | 1対1対応 |
| boolean | boolean | 1対1対応 |
| null | null | 1対1対応 |
| object | object (プレーンオブジェクト) | 1対1対応 |
| array | Array | 1対1対応 |
| (非対応) | undefined | シリアライズ時にプロパティ削除または配列要素がnullに変換 |
| (非対応) | Date | ISO 8601文字列に変換 逆変換は行われない。 |
| (非対応) | Function | シリアライズ時にプロパティ削除または配列要素がnullに変換 |
| (非対応) | Map / Set | {} (空オブジェクト) または [] (空配列) に変換 |
| (非対応) | RegExp | {} (空オブジェクト) に変換 |
| (非対応) | BigInt | TypeError が発生する。 |
JSON.parse
基本的な使用方法
JSON.parse(text) は、JSON文字列をJavaScriptの値に変換するメソッドである。
引数に渡した文字列が有効なJSONでない場合は SyntaxError をスローする。
基本的な使用例を以下に示す。
// JSON文字列をオブジェクトに変換する
const obj = JSON.parse('{"name":"Alice","age":30}');
console.log(obj.name); // "Alice"
console.log(obj.age); // 30
// 配列のJSONを変換する
const arr = JSON.parse('[1, 2, 3]');
console.log(arr); // [1, 2, 3]
// プリミティブ値のJSONを変換する
const num = JSON.parse('42');
console.log(num); // 42
const str = JSON.parse('"hello"');
console.log(str); // "hello"
reviver関数
JSON.parse(text, reviver) の第2引数にreviver関数を指定することにより、各プロパティの値を変換しながらパースできる。
下表に、reviver関数の仕様を示す。
| 項目 | 説明 |
|---|---|
| シグネチャ | function reviver(key, value) の形式で定義する。key はプロパティ名 (文字列)、value は変換前の値である。 |
| 処理順序 | 深さ優先探索で葉から根へ処理される。 |
| undefined を返した場合 | そのプロパティは削除される。 |
| ルートオブジェクトの処理 | 空文字列 ("") をキーとして最後に処理される。 |
reviver関数の主な活用例として、JSON文字列として保存されたDateオブジェクトの復元がある。
const jsonString = '{"name":"Alice","createdAt":"2024-01-15T10:30:00.000Z"}';
const data = JSON.parse(jsonString, (key, value) => {
// ISO 8601形式の文字列をDateオブジェクトに変換する
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
console.log(data.name); // "Alice"
console.log(data.createdAt); // Dateオブジェクト
console.log(data.createdAt instanceof Date); // true
プロパティを削除するreviver関数の例を以下に示す。
const json = '{"name":"Alice","password":"secret","age":30}';
const filtered = JSON.parse(json, (key, value) => {
// パスワードプロパティを除去する
if (key === 'password') return undefined;
return value;
});
console.log(filtered); // { name: "Alice", age: 30 }
エラーハンドリング
JSON.parse は無効なJSON文字列が渡された場合に SyntaxError をスローする。
そのため、外部から取得したJSON文字列をパースする際は try / catch で処理することが推奨される。
function safeParseJSON(text) {
try {
return { success: true, data: JSON.parse(text) };
}
catch (error) {
if (error instanceof SyntaxError) {
console.error('無効なJSON文字列:', error.message);
}
return { success: false, data: null };
}
}
// 有効なJSONの場合
const result1 = safeParseJSON('{"name":"Alice"}');
console.log(result1.success); // true
console.log(result1.data); // { name: "Alice" }
// 無効なJSONの場合 (末尾カンマ)
const result2 = safeParseJSON('{"name":"Alice",}');
console.log(result2.success); // false
console.log(result2.data); // null
JSON.stringify
基本的な使用方法
JSON.stringify(value, replacer, space) は、JavaScriptの値をJSON文字列に変換するメソッドである。
value- シリアライズ対象の値
replacer- フィルタリング関数 または 配列 (省略可能)
space- インデントの指定 (省略可能)
基本的な使用例を以下に示す。
const obj = { name: "Alice", age: 30, active: true };
// 基本的なシリアライズ
const json = JSON.stringify(obj);
console.log(json); // '{"name":"Alice","age":30,"active":true}'
// 配列のシリアライズ
const arr = [1, "hello", true, null];
console.log(JSON.stringify(arr)); // '[1,"hello",true,null]'
replacer引数
replacer関数
replacerとして関数を指定した場合、オブジェクトの各プロパティに対してその関数が呼び出される。
undefined、関数、または Symbol を返したプロパティは出力から除外される。
const user = {
id: 1,
name: "Alice",
password: "secret123",
apiKey: "key-xxxx-yyyy"
};
// 機密情報を除外するreplacer関数
const safeJson = JSON.stringify(user, (key, value) => {
if (key === 'password' || key === 'apiKey') return undefined;
return value;
});
console.log(safeJson); // '{"id":1,"name":"Alice"}'
// 数値を文字列に変換するreplacer関数
const data = { count: 42, label: "items" };
const converted = JSON.stringify(data, (key, value) => {
if (typeof value === 'number') return String(value);
return value;
});
console.log(converted); // '{"count":"42","label":"items"}'
replacer配列
replacerとして文字列の配列を指定した場合、配列に含まれるキーのプロパティのみが出力される。(ホワイトリスト方式)
const user = {
id: 1,
name: "Alice",
email: "alice@example.com",
password: "secret",
role: "admin"
};
// id, name, emailのみを含む
const publicInfo = JSON.stringify(user, ['id', 'name', 'email']);
console.log(publicInfo); // '{"id":1,"name":"Alice","email":"alice@example.com"}'
space引数
space 引数を指定することにより、出力JSONに読みやすいインデントを追加できる。
- 数値を指定した場合
- 指定したスペース数でインデントされる。(最大10)
- 文字列を指定した場合
- その文字列でインデントされる。(最大10文字)
const obj = { name: "Alice", scores: [85, 92, 78] };
// 数値でインデント
console.log(JSON.stringify(obj, null, 2));
// {
// "name": "Alice",
// "scores": [
// 85,
// 92,
// 78
// ]
// }
// タブ文字でインデント
console.log(JSON.stringify(obj, null, '\t'));
// {
// "name": "Alice",
// "scores": [...]
// }
toJSON()メソッド
オブジェクトに toJSON() メソッドが定義されている場合、JSON.stringify はオブジェクト自体ではなく toJSON() の戻り値をシリアライズする。
Date オブジェクトはこの仕組みを組み込みで持っており、toISOString() と同等の結果を返す。
// DateオブジェクトのtoJSON()
const date = new Date('2024-01-15T10:30:00.000Z');
console.log(JSON.stringify(date)); // '"2024-01-15T10:30:00.000Z"'
// カスタムクラスでのtoJSON()実装
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 9 / 5 + 32;
}
toJSON() {
// シリアライズ時にcelsiusとfahrenheitの両方を含める
return {
celsius: this.celsius,
fahrenheit: this.fahrenheit
};
}
}
const temp = new Temperature(100);
console.log(JSON.stringify(temp));
// '{"celsius":100,"fahrenheit":212}'
シリアライズの注意点
無視・変換される値
JSON.stringify のシリアライズ時に、JavaScriptの値がどのように変換されるか下表に示す。
| 型 | オブジェクトプロパティ値 | 配列要素 | トップレベル |
|---|---|---|---|
| undefined | 省略 (プロパティが削除) | null | undefined (文字列化されない) |
| function | 省略 (プロパティが削除) | null | undefined (文字列化されない) |
| Symbol | 省略 (プロパティが削除) | null | undefined (文字列化されない) |
| NaN | "null" (文字列) | "null" (文字列) | "null" (文字列) |
| Infinity | "null" (文字列) | "null" (文字列) | "null" (文字列) |
| BigInt | TypeError が発生 | TypeError が発生 | TypeError が発生 |
具体的な動作例を以下に示す。
const obj = {
name: "Alice",
fn: function() {}, // 省略される
sym: Symbol("id"), // 省略される
undef: undefined, // 省略される
nan: NaN, // null になる
inf: Infinity // null になる
};
console.log(JSON.stringify(obj));
// '{"name":"Alice","nan":null,"inf":null}'
// 配列要素では、nullに変換される
const arr = [1, undefined, function() {}, Symbol("x"), NaN];
console.log(JSON.stringify(arr));
// '[1,null,null,null,null]'
// BigIntはTypeErrorをスローする
try {
JSON.stringify({ value: 42n });
}
catch (e) {
console.log(e instanceof TypeError); // true
}
Dateオブジェクト
Date オブジェクトは toJSON() メソッドを持ち、toISOString() と同等のISO 8601形式の文字列に自動変換される。
ただし、JSON.parse は文字列をDateオブジェクトに戻す機能を持たないため、reviver関数を使用して明示的に変換する必要がある。
// シリアライズ : DateがISO文字列に変換される
const event = {
title: "ミーティング",
date: new Date("2024-06-15T09:00:00.000Z")
};
const json = JSON.stringify(event);
console.log(json);
// '{"title":"ミーティング","date":"2024-06-15T09:00:00.000Z"}'
// デシリアライズ : reviver関数でDateを復元する
const restored = JSON.parse(json, (key, value) => {
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
console.log(restored.date instanceof Date); // true
console.log(restored.date.getFullYear()); // 2024
循環参照
オブジェクトが循環参照を持つ場合、JSON.stringify は TypeError をスローする。
WeakSet を使用することで、循環参照を検出してその箇所を除外するreplacer関数を定義できる。
// 循環参照を持つオブジェクト
const obj = { name: "Alice" };
obj.self = obj; // 循環参照
try {
JSON.stringify(obj);
}
catch (e) {
console.log(e instanceof TypeError); // true
console.log(e.message); // "Converting circular structure to JSON"
}
// WeakSetを使用した循環参照の回避
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return undefined; // 循環参照をundefinedで置換して除外する
}
seen.add(value);
}
return value;
};
};
const safeJson = JSON.stringify(obj, getCircularReplacer());
console.log(safeJson); // '{"name":"Alice"}'
Map / Set / RegExp
Map、Set、RegExp はいずれもJSONで直接表現できないため、シリアライズ時に情報が失われる。
これらの型を正しくシリアライズするには、事前に変換処理が必要である。
// MapとSetはそのままでは空オブジェクト / 配列になる
const map = new Map([["key1", "value1"], ["key2", "value2"]]);
const set = new Set([1, 2, 3]);
const regex = /hello/gi;
console.log(JSON.stringify({ map, set, regex }));
// '{"map":{},"set":{},"regex":{}}' // 全て空オブジェクト
// Mapのシリアライズ: Object.fromEntries() または スプレッド構文で変換する
const mapAsObject = Object.fromEntries(map);
console.log(JSON.stringify(mapAsObject));
// '{"key1":"value1","key2":"value2"}'
// Mapをエントリ配列として保存する場合
const mapAsArray = [...map];
console.log(JSON.stringify(mapAsArray));
// '[["key1","value1"],["key2","value2"]]'
// Setのシリアライズ: 配列に変換する
const setAsArray = [...set];
console.log(JSON.stringify(setAsArray));
// '[1,2,3]'
// RegExpのシリアライズ: sourceとflagsを保存する
const regexObj = { source: regex.source, flags: regex.flags };
console.log(JSON.stringify(regexObj));
// '{"source":"hello","flags":"gi"}'
// RegExpの復元
const restoredRegex = new RegExp(regexObj.source, regexObj.flags);
console.log(restoredRegex); // /hello/gi
ディープコピー
JSON.parse(JSON.stringify())によるディープコピー
JSON.parse(JSON.stringify(obj)) は、オブジェクトのディープコピーを作成する最も単純な手法である。
外部ライブラリなしで定義でき、パフォーマンスも良好であるが、JSONで表現できない型は失われる点に注意が必要である。
const original = {
name: "Alice",
scores: [85, 92, 78],
address: {
city: "Tokyo",
country: "Japan"
}
};
// ディープコピーを作成する
const copy = JSON.parse(JSON.stringify(original));
// コピーを変更しても元のオブジェクトに影響しない
copy.scores.push(100);
copy.address.city = "Osaka";
console.log(original.scores); // [85, 92, 78] (変更されない)
console.log(original.address.city); // "Tokyo" (変更されない)
// 注意 : JSONで表現できない型は失われる
const withSpecialTypes = {
date: new Date("2024-01-01"),
fn: function() {},
undef: undefined,
regex: /hello/
};
const copied = JSON.parse(JSON.stringify(withSpecialTypes));
console.log(typeof copied.date); // "string" (Dateが文字列になる)
console.log(copied.fn); // undefined (関数が削除)
console.log(copied.undef); // undefined (プロパティが存在しない)
console.log(copied.regex); // {} (RegExpが空オブジェクトになる)
structuredClone()との比較
structuredClone() は、Structured Clone アルゴリズムを使用してオブジェクトのディープコピーを作成する組み込み関数である。
Date、Map、Set、循環参照を正しく処理できる点がJSON方式と異なる。
Webブラウザの対応状況として、Chrome 98+, Firefox 94+, Safari 15.4+, Node.js 17.0+ 以降で使用できる。
| データ型 | JSON方式 | structuredClone() |
|---|---|---|
| 基本型 (string, number, boolean, null) | 正常にコピー | 正常にコピー |
| Date | 文字列に変換 (型が変わる) | Dateオブジェクトとして正しくコピー |
| Map | 空オブジェクト {} (情報が失われる) | Mapとして正しくコピー |
| Set | 空オブジェクト {} (情報が失われる) | Setとして正しくコピー |
| RegExp | 空オブジェクト {} (情報が失われる) | フラグを含めて正しくコピー |
| 関数 | 削除される | TypeError が発生 (コピー不可) |
| undefined | 削除される | 正常にコピー |
| Symbol | 削除される | TypeError が発生 (コピー不可) |
| BigInt | TypeError が発生 | 正常にコピー |
| 循環参照 | TypeError が発生 | 正常に処理 |
structuredClone() の使用例を以下に示す。
const original = {
name: "Alice",
createdAt: new Date("2024-01-15"),
tags: new Set(["js", "web"]),
metadata: new Map([["version", "1.0"]])
};
// structuredClone()でコピーする
const copy = structuredClone(original);
console.log(copy.createdAt instanceof Date); // true (Dateが保持される)
console.log(copy.tags instanceof Set); // true (Setが保持される)
console.log(copy.metadata instanceof Map); // true (Mapが保持される)
// 循環参照も正しくコピーできる
const circular = { name: "test" };
circular.self = circular;
const circularCopy = structuredClone(circular);
console.log(circularCopy.self === circularCopy); // true (循環参照が維持される)
TauriでのJSON活用
Tauri v2では、JavaScriptフロントエンドとRustバックエンド間のIPC通信において、JSON形式でデータが受け渡される。
invoke() 関数の引数と戻り値は自動的にシリアライズ・デシリアライズされる。
データフローを以下に示す。
- JavaScript側 -->
JSON.stringify(自動) --> IPC通信 - IPC通信 -->
serde_json(自動) --> Rust構造体 - Rust構造体 -->
serde_json(自動) --> IPC通信 - IPC通信 -->
JSON.parse(自動) --> JavaScript側
invoke()によるデータ受け渡し
invoke() はJavaScriptからRustのコマンドを呼び出す関数であり、常にPromiseを返す。
引数はオブジェクト形式で渡し、Rust側のコマンドのパラメータ名と一致させる必要がある。
import { invoke } from '@tauri-apps/api/core';
// 文字列を送受信する基本的な例
const result = await invoke('my_command', { message: 'Hello, Tauri!' });
console.log(result); // Rust側から返ってきた文字列
// オブジェクトを送受信する例
const userData = {
firstName: "Alice",
lastName: "Smith",
isPremium: true
};
const response = await invoke('process_user', { user: userData });
console.log(response);
対応するRust側のコマンド定義を以下に示す。
#[tauri::command]
fn my_command(message: String) -> String {
format!("受信: {}", message)
}
Rustの構造体とJSONの対応
RustでJSONとの相互変換を行うには、serde クレート と serde_json クレートを使用する。
#[derive(Serialize, Deserialize)] アトリビュートを付与することにより、構造体の自動変換が有効になる。
JavaScriptではキャメルケース、Rustではスネークケースが慣習となっているため、#[serde(rename_all = "camelCase")] アトリビュートを使用してフィールド名の変換ルールを設定することが推奨される。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UserData {
first_name: String, // JSON上では: firstName
last_name: String, // JSON上では: lastName
is_premium: bool // JSON上では: isPremium
}
#[tauri::command]
fn process_user(user: UserData) -> UserData {
println!("名前: {} {}", user.first_name, user.last_name);
user // そのまま返す
}
JavaScript側から以下に示すように呼び出す。
import { invoke } from '@tauri-apps/api/core';
// camelCaseのキーでオブジェクトを送る
const result = await invoke('process_user', {
user: {
firstName: "Alice",
lastName: "Smith",
isPremium: true
}
});
// 戻り値もcamelCaseで受け取る
console.log(result.firstName); // "Alice"
console.log(result.isPremium); // true
複雑なデータ構造の受け渡し
Rustの主要なデータ型とJSONの対応を以下に示す。
Vec<T> (配列)
Rustの Vec<T> はJSONの配列として受け渡される。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Item {
id: u32,
name: String
}
#[tauri::command]
fn process_items(items: Vec<Item>) -> u32 {
items.len() as u32
}
import { invoke } from '@tauri-apps/api/core';
const count = await invoke('process_items', {
items: [
{ id: 1, name: "Item A" },
{ id: 2, name: "Item B" }
]
});
console.log(count); // 2
Option<T> (null許容値)
Rustの Option<T> は、Some(value) が値そのものに、None が null にマッピングされる。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Profile {
name: String,
middle_name: Option<String> // null の可能性あり
}
#[tauri::command]
fn get_profile(has_middle: bool) -> Profile {
Profile {
name: "Alice".to_string(),
middle_name: if has_middle { Some("Marie".to_string()) } else { None }
}
}
import { invoke } from '@tauri-apps/api/core';
const profile1 = await invoke('get_profile', { hasMiddle: true });
console.log(profile1.middleName); // "Marie"
const profile2 = await invoke('get_profile', { hasMiddle: false });
console.log(profile2.middleName); // null
Enum (列挙型)
Rustの列挙型は #[serde(tag = "type")] アトリビュートを使用することで、JavaScriptのオブジェクトにマッピングできる。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum RequestType {
Create { name: String },
Update { id: u32, name: String },
Delete { id: u32 }
}
#[tauri::command]
fn handle_request(request: RequestType) -> String {
match request {
RequestType::Create { name } => format!("作成: {}", name),
RequestType::Update { id, name } => format!("更新: id={}, name={}", id, name),
RequestType::Delete { id } => format!("削除: id={}", id)
}
}
import { invoke } from '@tauri-apps/api/core';
// Createリクエスト
const res1 = await invoke('handle_request', {
request: { type: 'Create', name: 'New Item' }
});
console.log(res1); // "作成: New Item"
// Updateリクエスト
const res2 = await invoke('handle_request', {
request: { type: 'Update', id: 42, name: 'Updated Item' }
});
console.log(res2); // "更新: id=42, name=Updated Item"
// Deleteリクエスト
const res3 = await invoke('handle_request', {
request: { type: 'Delete', id: 42 }
});
console.log(res3); // "削除: id=42"
関連情報
- JavaScriptの基礎 - Fetch API
- Promiseを返すFetch APIによるHTTPリクエストの定義
- JavaScriptの基礎 - Promise
- Promiseの基本構文から静的メソッド、Tauriでの使用例
- JavaScriptの基礎 - async await
- Promiseをより簡潔に記述するためのasync / await構文の詳細
- JavaScriptの基礎 - エラーハンドリング
- try / catch、カスタムエラー、エラーの伝播