JSON

この章では、JavaScriptと密接な関係にあるJSONというデータフォーマットについて見ていきます。

JSONとは

JSONはJavaScript Object Notationの略で、JavaScriptのオブジェクトリテラルをベースに作られた軽量なデータフォーマットです。 JSONの仕様はECMA-404として標準化されています。 JSONは、人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行える形式になっています。 そのため、多くのプログラミング言語が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形式の文字列と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メソッドは第一引数に与えられたオブジェクトを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つあります。 第二引数はreplacer引数とも呼ばれ、関数あるいは配列を渡せます。 関数を渡した場合は引数にプロパティのキーと値が渡され、その返り値によって文字列に変換される際の挙動をコントロールできます。 次の例は値が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"}'

第三引数は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 {}
BigInt 例外が発生する

// 値が関数のプロパティ
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":{}}'

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

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

const obj = { foo: "foo" };
obj.self = obj;
try {
    JSON.stringify(obj);
} catch (error) {
    console.error(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メソッドは自作のクラスを特殊な形式でシリアライズする目的などに使われます。

まとめ

この章では、JSONについて学びました。

  • JSONはJavaScriptのオブジェクトリテラルをベースに作られた軽量なデータフォーマット
  • JSONオブジェクトを使ったシリアライズとデシリアライズ
  • JSON形式にシリアライズできないオブジェクトもある
  • JSON.stringifyはシリアライズ対象のtoJSONメソッドを利用する