JavaScript for impatient programmers (ES2022 edition)
この本のサポートをお願いします: 購入 または 寄付
(広告です。ブロックしないでください。)

45 JSON (JSON) の作成とパース



JSON(「JavaScript Object Notation」)は、テキストを使用してデータをエンコードするストレージ形式です。その構文は、JavaScript式のサブセットです。例として、ファイル`jane.json`に保存されている次のテキストを考えてみましょう。

{
  "first": "Jane",
  "last": "Porter",
  "married": true,
  "born": 1890,
  "friends": [ "Tarzan", "Cheeta" ]
}

JavaScriptには、JSONの作成とパースのためのメソッドを提供するグローバル名前空間オブジェクト`JSON`があります。

45.1 JSONの発見と標準化

JSONの仕様は、2001年にDouglas Crockfordによってjson.orgで公開されました。彼は次のように説明しています。

私はJSONを発見しました。JSONはすでに自然界に存在していたため、発明したとは主張しません。私がしたのは、それを見つけ、名前を付け、それがどのように役立つかを説明したことです。私が最初に発見した人物であるとは主張しません。少なくとも1年前には、他の誰かが発見していたことを知っています。私が発見した最も初期の事例は、Netscapeの誰かが、1996年という早い時期に、データ通信を行うためにJavaScriptの配列リテラルを使用していたことです。これは、私がそのアイデアに偶然出会う少なくとも5年前のことです。

その後、JSONはECMA-404として標準化されました。

45.1.1 JSONの文法は固定されている

ECMA-404規格からの引用

JSONの文法は非常にシンプルであるため、変更されることはないと予想されます。これにより、JSONは基礎となる表記法として、非常に安定したものになります。

したがって、JSONは、オプションの末尾のカンマ、コメント、引用符のないキーなど、望ましいかどうかに関係なく、改善されることはありません。ただし、プレーンJSONにコンパイルされるJSONのスーパーセットを作成する余地は残されています。

45.2 JSON構文

JSONは、JavaScriptの次の部分で構成されています。

結果として、JSONでは循環構造を(直接)表現することはできません。

45.3 `JSON` APIの使用

グローバル名前空間オブジェクト`JSON`には、JSONデータを操作するためのメソッドが含まれています。

45.3.1 `JSON.stringify(data, replacer?, space?)`

`.stringify()`は、JavaScriptの`data`をJSON文字列に変換します。このセクションでは、パラメータ`replacer`は無視しています。 §45.4「文字列化とパースのカスタマイズ」で説明しています。

45.3.1.1 結果: 1行のテキスト

最初の引数のみを指定すると、`.stringify()`は1行のテキストを返します。

assert.equal(
  JSON.stringify({foo: ['a', 'b']}),
  '{"foo":["a","b"]}' );
45.3.1.2 結果: インデントされた行のツリー

`space`に負でない整数を指定すると、`.stringify()`は1行以上の行を返し、ネストのレベルごとに`space`スペースでインデントします。

assert.equal(
JSON.stringify({foo: ['a', 'b']}, null, 2),
`{
  "foo": [
    "a",
    "b"
  ]
}`);
45.3.1.3 JavaScriptデータの文字列化方法の詳細

プリミティブ値

オブジェクト

45.3.2 `JSON.parse(text, reviver?)`

`.parse()`は、JSON`text`をJavaScript値に変換します。このセクションでは、パラメータ`reviver`は無視しています。 §45.4「文字列化とパースのカスタマイズ」で説明しています。

これは、`.parse()`の使用例です。

> JSON.parse('{"foo":["a","b"]}')
{ foo: [ 'a', 'b' ] }

45.3.3 例: JSONへの変換とJSONからの変換

次のクラスは、JSONからの(行A)およびJSONへの(行B)変換を実装しています。

class Point {
  static fromJson(jsonObj) { // (A)
    return new Point(jsonObj.x, jsonObj.y);
  }

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  toJSON() { // (B)
    return {x: this.x, y: this.y};
  }
}

  演習: オブジェクトをJSONに変換する、およびJSONから変換する

exercises/json/to_from_json_test.mjs

45.4 文字列化とパースのカスタマイズ (上級)

文字列化とパースは、次のようにカスタマイズできます。

45.4.1 `.stringfy()`: オブジェクトのどのプロパティを文字列化するかを指定する

`.stringify()`の2番目のパラメータが配列の場合、そこに名前が記載されているオブジェクトプロパティのみが結果に含まれます。

const obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  }
};
assert.equal(
  JSON.stringify(obj, ['b', 'c']),
  '{"b":{"c":2}}');

45.4.2 `.stringify()`と`.parse()`: 値ビジター

私が*値ビジター*と呼んでいるのは、JavaScriptデータを変換する関数です。

このセクションでは、JavaScriptデータは値のツリーと見なされます。データがアトミックな場合、ルートのみを持つツリーです。ツリー内のすべての値は、一度に1つずつ値ビジターに送られます。ビジターが返す内容に応じて、現在の値は省略、変更、または保持されます。

値ビジターには、次の型のシグネチャがあります。

type ValueVisitor = (key: string, value: any) => any;

パラメータは次のとおりです。

値ビジターは、次のいずれかを返すことができます。

45.4.3 例: 値の訪問

次のコードは、値ビジターが値を認識する順序を示しています。

const log = [];
function valueVisitor(key, value) {
  log.push({this: this, key, value});
  return value; // no change
}

const root = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  }
};
JSON.stringify(root, valueVisitor);
assert.deepEqual(log, [
  { this: { '': root }, key: '',  value: root   },
  { this: root        , key: 'a', value: 1      },
  { this: root        , key: 'b', value: root.b },
  { this: root.b      , key: 'c', value: 2      },
  { this: root.b      , key: 'd', value: 3      },
]);

ご覧のとおり、`JSON.stringify()`のreplacerは、値をトップダウン(ルートが最初、リーフが最後)で訪問します。その方向に進む理由は、JavaScriptの値をJSONの値に変換しているためです。また、単一のJavaScriptオブジェクトは、JSON互換の値のツリーに展開される場合があります。

対照的に、`JSON.parse()`のreviverは、値をボトムアップ(リーフが最初、ルートが最後)で訪問します。その方向に進む理由は、JSON値をJavaScript値に組み立てているためです。したがって、全体を変換する前に、部品を変換する必要があります。

45.4.4 例: サポートされていない値の文字列化

`JSON.stringify()`は、正規表現オブジェクトを特別にサポートしていません。プレーンオブジェクトであるかのように文字列化します。

const obj = {
  name: 'abc',
  regex: /abc/ui,
};
assert.equal(
  JSON.stringify(obj),
  '{"name":"abc","regex":{}}');

これは、replacerを使用して修正できます。

function replacer(key, value) {
  if (value instanceof RegExp) {
    return {
      __type__: 'RegExp',
      source: value.source,
      flags: value.flags,
    };
  } else {
    return value; // no change
  }
}
assert.equal(
JSON.stringify(obj, replacer, 2),
`{
  "name": "abc",
  "regex": {
    "__type__": "RegExp",
    "source": "abc",
    "flags": "iu"
  }
}`);

45.4.5 例: サポートされていない値のパース

前のセクションの結果を`JSON.parse()`するには、reviverが必要です。

function reviver(key, value) {
  // Very simple check
  if (value && value.__type__ === 'RegExp') {
    return new RegExp(value.source, value.flags);
  } else {
    return value;
  }
}
const str = `{
  "name": "abc",
  "regex": {
    "__type__": "RegExp",
    "source": "abc",
    "flags": "iu"
  }
}`;
assert.deepEqual(
  JSON.parse(str, reviver),
  {
    name: 'abc',
    regex: /abc/ui,
  });

45.5 FAQ

45.5.1 JSONはなぜコメントをサポートしないのか?

Douglas Crockfordは、2012年5月1日のGoogle+投稿で、その理由を説明しています。

JSONからコメントを削除したのは、人々がコメントを使用してパースディレクティブを保持しているのを見たからです。これは、相互運用性を破壊してしまう慣行です。コメントがないと悲しくなる人がいることは知っていますが、そうあるべきではありません。

JSONを使用して、注釈を付けたい構成ファイルを保持しているとします。好きなだけコメントを挿入してください。次に、JSONパーサーに渡す前に、JSMin [JavaScriptのミニファイアー]を介してパイプします。