2024年1月24日

JSON メソッド、toJSON

複雑なオブジェクトがあり、それをネットワーク経由で送信したり、ログ出力のために文字列に変換したいとします。

当然、そのような文字列にはすべての重要なプロパティが含まれている必要があります。

変換は次のように実装できます。

let user = {
  name: "John",
  age: 30,

  toString() {
    return `{name: "${this.name}", age: ${this.age}}`;
  }
};

alert(user); // {name: "John", age: 30}

…しかし、開発プロセスでは、新しいプロパティが追加され、古いプロパティの名前が変更され、削除されます。 そのような`toString`を毎回更新するのは面倒になる可能性があります。 プロパティをループ処理することもできますが、オブジェクトが複雑で、プロパティにネストされたオブジェクトがある場合はどうでしょうか? その変換も実装する必要があります。

幸いなことに、これらすべてを処理するコードを書く必要はありません。 このタスクはすでに解決されています。

JSON.stringify

JSON (JavaScript Object Notation) は、値とオブジェクトを表すための一般的な形式です。 RFC 4627 標準で説明されています。 最初はJavaScript用に作られましたが、他の多くの言語にもそれを処理するためのライブラリがあります。 したがって、クライアントがJavaScriptを使用し、サーバーがRuby/PHP/Java/その他で記述されている場合、データ交換にJSONを簡単に使用できます。

JavaScriptは次のメソッドを提供します。

  • オブジェクトをJSONに変換するための `JSON.stringify`。
  • JSONをオブジェクトに戻すための `JSON.parse`。

たとえば、ここでは生徒を `JSON.stringify` します。

let student = {
  name: 'John',
  age: 30,
  isAdmin: false,
  courses: ['html', 'css', 'js'],
  spouse: null
};

let json = JSON.stringify(student);

alert(typeof json); // we've got a string!

alert(json);
/* JSON-encoded object:
{
  "name": "John",
  "age": 30,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "spouse": null
}
*/

`JSON.stringify(student)` メソッドはオブジェクトを受け取り、文字列に変換します.

結果の `json` 文字列は、 *JSONエンコード* または *シリアライズ* または *文字列化* または *マーシャリング* されたオブジェクトと呼ばれます。 ネットワーク経由で送信したり、プレーンデータストアに配置する準備ができました。

JSONでエンコードされたオブジェクトは、オブジェクトリテラルとはいくつかの重要な違いがあることに注意してください。

  • 文字列は二重引用符を使用します。 JSONに単一引用符またはバッククォートはありません。 したがって、`'John'` は `"John"` になります。
  • オブジェクトプロパティ名も二重引用符で囲まれています。 これは必須です。 したがって、`age:30` は `"age":30` になります。

`JSON.stringify` はプリミティブにも適用できます。

JSONは次のデータ型をサポートしています。

  • オブジェクト `{ ... }`
  • 配列 `[ ... ]`
  • プリミティブ
    • 文字列、
    • 数値、
    • ブール値 `true/false`、
    • null.

例えば

// a number in JSON is just a number
alert( JSON.stringify(1) ) // 1

// a string in JSON is still a string, but double-quoted
alert( JSON.stringify('test') ) // "test"

alert( JSON.stringify(true) ); // true

alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]

JSONはデータのみの言語に依存しない仕様であるため、一部のJavaScript固有のオブジェクトプロパティは `JSON.stringify` によってスキップされます。

すなわち

  • 関数プロパティ(メソッド)。
  • シンボルキーと値。
  • `undefined` を格納するプロパティ。
let user = {
  sayHi() { // ignored
    alert("Hello");
  },
  [Symbol("id")]: 123, // ignored
  something: undefined // ignored
};

alert( JSON.stringify(user) ); // {} (empty object)

通常は問題ありません。 それが望まないものである場合、すぐにプロセスをカスタマイズする方法を見ていきます。

素晴らしい点は、ネストされたオブジェクトがサポートされ、自動的に変換されることです。

例えば

let meetup = {
  title: "Conference",
  room: {
    number: 23,
    participants: ["john", "ann"]
  }
};

alert( JSON.stringify(meetup) );
/* The whole structure is stringified:
{
  "title":"Conference",
  "room":{"number":23,"participants":["john","ann"]},
}
*/

重要な制限:循環参照があってはなりません。

例えば

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: ["john", "ann"]
};

meetup.place = room;       // meetup references room
room.occupiedBy = meetup; // room references meetup

JSON.stringify(meetup); // Error: Converting circular structure to JSON

ここでは、循環参照のため、変換は失敗します。`room.occupiedBy` は `meetup` を参照し、`meetup.place` は `room` を参照します。

除外と変換:replacer

`JSON.stringify` の完全な構文は次のとおりです。

let json = JSON.stringify(value[, replacer, space])
value
エンコードする値。
replacer
エンコードするプロパティの配列、またはマッピング関数 `function(key, value)`。
space
フォーマットに使用するスペースの量

ほとんどの場合、`JSON.stringify` は最初の引数のみで使用されます。 しかし、循環参照を除外するなど、置換プロセスを微調整する必要がある場合は、`JSON.stringify` の2番目の引数を使用できます。

プロパティの配列を渡すと、これらのプロパティのみがエンコードされます。

例えば

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}

ここではおそらく厳しすぎます。 プロパティリストはオブジェクト構造全体に適用されます。 したがって、`name` がリストにないため、`participants` のオブジェクトは空です。

循環参照を引き起こす `room.occupiedBy` を除くすべてのプロパティをリストに含めましょう。

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Conference",
  "participants":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

これで、`occupiedBy` 以外のすべてがシリアライズされます。 しかし、プロパティのリストはかなり長いです。

幸いなことに、配列の代わりに`replacer`として関数を使用できます。

関数はすべての `(key, value)` ペアに対して呼び出され、元の値の代わりに使用される「置換された」値を返す必要があります。 値をスキップする場合は `undefined` を返します。

この場合、`occupiedBy` 以外のすべてに対して `value` を「そのまま」返すことができます。 `occupiedBy` を無視するために、以下のコードは `undefined` を返します。

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* key:value pairs that come to replacer:
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
occupiedBy: [object Object]
*/

`replacer` 関数は、ネストされたオブジェクトや配列項目を含むすべてのキー/値ペアを取得することに注意してください。 再帰的に適用されます。 `replacer` 内の `this` の値は、現在のプロパティを含むオブジェクトです。

最初の呼び出しは特別です。 これは、特別な「ラッパーオブジェクト」:`{"": meetup}` を使用して行われます。 つまり、最初の `(key, value)` ペアは空のキーを持ち、値はターゲットオブジェクト全体です。 上記の例で最初の行が `":[object Object]"` であるのはそのためです。

この考え方は、`replacer` にできるだけ多くのパワーを提供することです。 必要に応じて、オブジェクト全体を分析して置換/スキップする機会があります。

フォーマット:space

`JSON.stringify(value, replacer, space)` の3番目の引数は、プリティフォーマットに使用するスペースの数です。

以前は、すべての文字列化されたオブジェクトにはインデントと余分なスペースがありませんでした。 オブジェクトをネットワーク経由で送信する場合、これは問題ありません。 `space` 引数は、見栄えの良い出力のためだけに使用されます。

ここでは、`space = 2` は、ネストされたオブジェクトを複数行に表示し、オブジェクト内に2スペースのインデントを付けるようにJavaScriptに指示します。

let user = {
  name: "John",
  age: 25,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};

alert(JSON.stringify(user, null, 2));
/* two-space indents:
{
  "name": "John",
  "age": 25,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

/* for JSON.stringify(user, null, 4) the result would be more indented:
{
    "name": "John",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

3番目の引数は文字列にすることもできます。 この場合、スペースの数ではなく、文字列がインデントに使用されます。

`space` パラメーターは、ログ出力と見栄えの良い出力の目的にのみ使用されます。

カスタム「toJSON」

文字列変換の `toString` と同様に、オブジェクトはtoJSON変換のために `toJSON` メソッドを提供する場合があります。 `JSON.stringify` は、使用可能な場合は自動的にそれを呼び出します。

例えば

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  date: new Date(Date.UTC(2017, 0, 1)),
  room
};

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "date":"2017-01-01T00:00:00.000Z",  // (1)
    "room": {"number":23}               // (2)
  }
*/

ここでは、`date` `(1)` が文字列になったことがわかります。 これは、すべての日付に、このような種類の文字列を返す組み込みの `toJSON` メソッドがあるためです。

それでは、オブジェクト `room` `(2)` にカスタム `toJSON` を追加しましょう。

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};

let meetup = {
  title: "Conference",
  room
};

alert( JSON.stringify(room) ); // 23

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "room": 23
  }
*/

ご覧のとおり、`toJSON` は、直接呼び出し `JSON.stringify(room)` と `room` が別のエンコードされたオブジェクトにネストされている場合の両方で使用されます。

JSON.parse

JSON文字列をデコードするには、JSON.parse という別のメソッドが必要です。

構文

let value = JSON.parse(str[, reviver]);
str
解析するJSON文字列。
reviver
各 `(key, value)` ペアに対して呼び出され、値を変換できるオプションの関数(key,value)。

例えば

// stringified array
let numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

alert( numbers[1] ); // 1

または、ネストされたオブジェクトの場合

let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';

let user = JSON.parse(userData);

alert( user.friends[1] ); // 1

JSONは必要に応じて複雑にすることができ、オブジェクトと配列には他のオブジェクトと配列を含めることができます。 ただし、同じJSON形式に従う必要があります。

手書きのJSONによくある間違いは次のとおりです(デバッグのために手書きする必要がある場合があります)。

let json = `{
  name: "John",                     // mistake: property name without quotes
  "surname": 'Smith',               // mistake: single quotes in value (must be double)
  'isAdmin': false                  // mistake: single quotes in key (must be double)
  "birthday": new Date(2000, 2, 3), // mistake: no "new" is allowed, only bare values
  "friends": [0,1,2,3]              // here all fine
}`;

さらに、JSONはコメントをサポートしていません。 JSONにコメントを追加すると、無効になります。

JSON5 という別の形式があり、引用符で囲まれていないキー、コメントなどが許可されています。 しかし、これはスタンドアロンライブラリであり、言語の仕様には含まれていません。

通常のJSONがそれほど厳密なのは、開発者が怠惰だからではなく、解析アルゴリズムの簡単、信頼性が高く、非常に高速な実装を可能にするためです。

reviverの使用

サーバーから文字列化された `meetup` オブジェクトを取得したとします。

次のようになります。

// title: (meetup title), date: (meetup date)
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

…そして今、私たちはそれを *デシリアライズ* し、JavaScriptオブジェクトに戻す必要があります。

`JSON.parse` を呼び出して実行しましょう。

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str);

alert( meetup.date.getDate() ); // Error!

おっと! エラー!

`meetup.date` の値は文字列であり、`Date` オブジェクトではありません。 `JSON.parse` は、その文字列を `Date` に変換する必要があることをどのように知ることができたのでしょうか?

すべての値を「そのまま」返す2番目の引数として、復活関数を `JSON.parse` に渡しましょう。ただし、`date` は `Date` になります。

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( meetup.date.getDate() ); // now works!

ちなみに、これはネストされたオブジェクトでも機能します。

let schedule = `{
  "meetups": [
    {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
    {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( schedule.meetups[1].date.getDate() ); // works!

まとめ

  • JSONは、ほとんどのプログラミング言語向けの独自の独立した標準とライブラリを持つデータ形式です。
  • JSONは、プレーンオブジェクト、配列、文字列、数値、ブール値、および `null` をサポートしています。
  • JavaScriptは、JSONにシリアライズするためのメソッドJSON.stringify と、JSONから読み取るためのメソッドJSON.parse を提供します。
  • どちらのメソッドも、スマートな読み取り/書き込みのための変換関数をサポートしています。
  • オブジェクトに `toJSON` がある場合、`JSON.stringify` によって呼び出されます。

タスク

重要度:5

`user` をJSONに変換し、別の変数に読み戻します。

let user = {
  name: "John Smith",
  age: 35
};
let user = {
  name: "John Smith",
  age: 35
};

let user2 = JSON.parse(JSON.stringify(user));
重要度:5

単純な循環参照の場合、問題のあるプロパティを名前でシリアライゼーションから除外できます。

しかし、循環参照と通常のプロパティの両方で使用されている場合があるため、名前だけでは使用できない場合があります。そのため、プロパティを値でチェックできます。

replacer 関数を記述して、すべてを文字列化しますが、meetup を参照するプロパティは削除します。

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  occupiedBy: [{name: "John"}, {name: "Alice"}],
  place: room
};

// circular references
room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  /* your code */
}));

/* result should be:
{
  "title":"Conference",
  "occupiedBy":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/
let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  occupiedBy: [{name: "John"}, {name: "Alice"}],
  place: room
};

room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  return (key != "" && value == meetup) ? undefined : value;
}));

/*
{
  "title":"Conference",
  "occupiedBy":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

ここでも、valuemeetup であることが正常な最初の呼び出しを除外するために、key=="" をテストする必要があります。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHub issue またはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code> タグを使用します。複数行の場合は、<pre> タグで囲みます。10行を超える場合は、サンドボックス(plnkrjsbincodepenなど)を使用します。