JavaScriptの基礎 - JSON

提供: MochiuWiki : SUSE, EC, PCB

📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)

概要

JSON (JavaScript Object Notation) は、JavaScriptのオブジェクトリテラル構文を基にした軽量なテキスト形式のデータ交換フォーマットである。
言語非依存の仕様として設計されており、JavaScriptに限らず多くのプログラミング言語でデータの送受信に広く使用されている。

JavaScriptでJSONを操作するための組み込みオブジェクト JSON は、2つのメソッドを提供する。

  • JSON.parse
    JSON文字列をJavaScriptの値 (オブジェクト、配列、プリミティブ) に変換する。
  • JSON.stringify
    JavaScriptの値をJSON文字列に変換 (シリアライズ) する。


シリアライズ時には、JavaScriptの全ての型がJSONとして表現できるわけではないことに注意が必要である。

undefined、関数、Symbol はオブジェクトプロパティから削除され、NaNInfinitynull に変換される。
また、Date オブジェクトはISO 8601形式の文字列に変換されるが、JSON.parse では自動的に Date に戻されない。

JSONを利用したディープコピー (JSON.parse(JSON.stringify())) は手軽な手法であるが、上記の型の制約がある。
現代のWebブラウザやNode.js環境では、DateMapSet、循環参照を正しく処理できる structuredClone() が利用できる。

Tauri v2では、invoke() によるIPC通信でフロントエンドとRustバックエンド間のデータがJSONを介して自動的にシリアライズ・デシリアライズされる。
Rust側では serde クレートにより、構造体のフィールド名変換 (snake_case / camelCase) も自動化される。


JSONの基本

JSONの構文規則

JSONは厳格な構文規則を持つテキストフォーマットである。
JavaScriptのオブジェクトリテラルとは異なる点があるため、注意が必要である。

主な構文規則を以下に示す。

JSONの主な構文規則
規則 説明
キー名は必ず二重引用符 (") で囲む
  • 単一引用符 (') は使用不可
  • { "name": "Alice" } は有効
    { name: "Alice" } は無効
文字列値は二重引用符で囲む "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型の対応
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関数の仕様を示す。

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.stringifyTypeError をスローする。
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

MapSetRegExp はいずれも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 アルゴリズムを使用してオブジェクトのディープコピーを作成する組み込み関数である。
DateMapSet、循環参照を正しく処理できる点が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) が値そのものに、Nonenull にマッピングされる。

 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"



関連情報