JSON

JSONとは

JSONはJavaScript Object Notationの略で、JavaScriptのオブジェクトリテラルをベースに作られた軽量なデータフォーマットです。 JSONの仕様はECMA-404によって標準化されています。 人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行なえる形式になっているため、 多くのプログラミング言語がJSONを扱う機能を備えています。

JSONはJavaScriptのオブジェクトリテラル、配列リテラル、各種プリミティブ型の値を組み合わせたものです。 ただしJSONとJavaScriptは一部の構文に違いがあります。 たとえばJSONでは、オブジェクトリテラルのキーを必ずダブルクオートで囲まなければいけません。 また、小数点から書き始める数値リテラルや、先頭がゼロから始まる数値リテラルも使えません。 これらは機械がパースしやすくするために仕様で定められた制約です。

{
    "object": { 
        "number": 1, 
        "string": "js-primer",
        "boolean": true,
        "null": null,
        "array": [1, 2, 3]
    }
}

JSONの細かい仕様に関してはjson.orgの日本語ドキュメントにわかりやすくまとまっているので、参考にするとよいでしょう。

JSONオブジェクト

JavaScriptでJSONを扱うには、ビルトインのJSONオブジェクトを利用します。 JSONオブジェクトはグローバルオブジェクトなので、どのスコープからでもアクセスできます。 JSONオブジェクトはJSON形式の文字列とJavaScriptのオブジェクトを相互に変換するためのparseメソッドとstringifyメソッドを提供します。

JSON文字列をオブジェクトに変換する

JSON.parseメソッドは引数に与えられた文字列をJSONとしてパースし、その結果をJavaScriptのオブジェクトとして返す関数です。 次のコードは簡単なJSON形式の文字列をJavaScriptのオブジェクトに変換する例です。

// JSONはダブルクオートのみを許容するため、シングルクォートでJSON文字列を記述
const json = '{ "id": 1, "name": "js-primer" }';
const obj = JSON.parse(json);
console.log(obj.id); // => 1
console.log(obj.name); // => "js-primer"

文字列がJSONの配列を表す場合は、JSON.parseメソッドの返り値も配列になります。

const json = "[1, 2, 3]";
console.log(JSON.parse(json)); // => [1, 2, 3]

与えられた文字列がJSON形式でパースできない場合は例外が投げられます。 また、実際のアプリケーションでJSONを扱うのは、外部のプログラムとデータを交換する用途がほとんどです。 外部のプログラムが送ってくるデータが常にJSONとして正しい保証はありません。 そのため、JSON.parseメソッドは基本的にtry-catch文で例外処理をするべきです。

const userInput = "not json value";
try {
    const json = JSON.parse(userInput);
} catch (error) {
    console.log("パースできませんでした");
}

オブジェクトをJSON文字列に変換する

JSON.stringifyメソッドは第1引数に与えられたオブジェクトをJSON形式の文字列に変換して返す関数です。 HTTP通信でサーバーにデータを送信するときや、 アプリケーションが保持している状態を外部に保存するときなどに必要になります。 次のコードはJavaScriptのオブジェクトをJSON形式の文字列に変換する例です。

const obj = { id: 1, name: "js-primer", bio: null };
console.log(JSON.stringify(obj)); // => '{"id":1,"name":"js-primer","bio":null}'

JSON.stringifyメソッドにはオプショナルな引数が2つあります。 第2引数はreplacer引数とも呼ばれ、変換後のJSONに含まれるプロパティ関数あるいは配列を渡せます。 関数を渡した場合は引数にプロパティのキーと値が渡され、その返り値によって文字列に変換される際の挙動をコントロールできます。 次の例は値がnullであるプロパティを除外してJSONに変換するreplacer引数の例です。 replacer引数の関数でundefinedが返されたプロパティは、変換後のJSONに含まれなくなります。

const obj = { id: 1, name: "js-primer", bio: null };
const replacer = (key, value) => {
    if (value === null) {
        return undefined;
    }
    return value;
};
console.log(JSON.stringify(obj, replacer)); // => '{"id":1,"name":"js-primer"}'

replacer引数に配列を渡した場合はプロパティのホワイトリストとして使われ、 その配列に含まれる名前のプロパティだけが変換されます。

const obj = { id: 1, name: "js-primer", bio: null };
const replacer = ["id", "name"];
console.log(JSON.stringify(obj, replacer)); // => '{"id":1,"name":"js-primer"}'

第3引数はspace引数とも呼ばれ、変換後のJSON形式の文字列を読みやすくフォーマットする際のインデントを設定できます。 数値を渡すとその数値分の長さのスペースで、文字列を渡すとその文字列でインデントされます。 次のコードはスペース2個でインデントされたJSONを得る例です。

const obj = { id: 1, name: "js-primer" };
// replacer引数を使わない場合はnullを渡して省略するのが一般的です
console.log(JSON.stringify(obj, null, 2)); 
/*
{
   "id": 1,
   "name": "js-primer"
}
*/

また、次のコードはタブ文字でインデントされたJSONを得る例です。

const obj = { id: 1, name: "js-primer" };
console.log(JSON.stringify(obj, null, "\t")); 
/*
{
   "id": 1,
   "name": "js-primer"
}
*/

[コラム] JSONにシリアライズできないオブジェクト

JSON.stringifyメソッドはJSONで表現可能な値だけをシリアライズします。 そのため、値が関数やSymbol、あるいはundefinedであるプロパティなどは変換されません。 ただし、配列の値としてそれらが見つかったときには例外的にnullに置き換えられます。 またキーがSymbolである場合にもシリアライズの対象外になります。 代表的な変換の例を次の表とサンプルコードに示します。

シリアライズ前の値 シリアライズ後の値
文字列・数値・真偽値 対応する値
null null
配列 配列
オブジェクト オブジェクト
関数 変換されない(配列のときはnull)
undefined 変換されない(配列のときはnull)
Symbol 変換されない(配列のときはnull)
RegExp {}
Map, Set {}

// 値が関数のプロパティ
console.log(JSON.stringify({ x: function() {} })); // => '{}'
// 値がSymbolのプロパティ
console.log(JSON.stringify({ x: Symbol("") })); // => '{}'
// 値がundefinedのプロパティ
console.log(JSON.stringify({ x: undefined })); // => '{}'
// 配列の場合
console.log(JSON.stringify({ x: [10, function() {}] })); // => '{"x":[10,null]}'
// キーがSymbolのプロパティ
JSON.stringify({ [Symbol("foo")]: "foo" }); // => '{}'
// 値がRegExpのプロパティ
console.log(JSON.stringify({ x: /foo/ })); // => '{"x":{}}'
// 値がMapのプロパティ
const map = new Map();
map.set("foo", "foo");
console.log(JSON.stringify({ x: map })); // => '{"x":{}}'

オブジェクトがシリアライズされる際は、そのオブジェクトの列挙可能なプロパティだけが再帰的にシリアライズされます。 RegExpやMap、Setなどのインスタンスは列挙可能なプロパティを持たないため、空のオブジェクトに変換されます。

また、JSON.stringifyメソッドがシリアライズに失敗することもあります。 よくあるのは、参照が循環しているオブジェクトをシリアライズしようとしたときに例外が投げられるケースです。 たとえば次の例のように、あるオブジェクトのプロパティを再帰的に辿って自分自身が見つかるような場合はシリアライズが不可能となります。 JSON.parseメソッドだけでなく、JSON.stringifyメソッドも例外処理をおこなって安全に使いましょう。

const obj = { foo: "foo" };
obj.self = obj;
try {
    JSON.stringify(obj);
} catch (error) {
    console.log(error); // => "TypeError: Converting circular structure to JSON"
}

[コラム] toJSONメソッドを使ったシリアライズ

オブジェクトがtoJSONメソッドを持っている場合、JSON.stringifyメソッドは既定の文字列変換ではなくtoJSONメソッドの返り値を使います。 次の例のように、引数に直接渡されたときだけでなく引数のプロパティとして登場したときにも再帰的に処理されます。

const obj = {
    foo: "foo",
    toJSON() {
        return "bar";
    }
};
console.log(JSON.stringify(obj)); // => '"bar"'
console.log(JSON.stringify({ x: obj })); // => '{"x":"bar"}'

toJSONメソッドは自作のクラスを特殊な形式でシリアライズする目的などに使われます。