前置き
JavaScriptの関数JSON.stringify
は、JavaScriptのオブジェクトや値をJSON文字列に変換します。第1引数に、変換したいオブジェクトや値を設定します。
let hoge = {
a: 1,
b: 'fuga',
c: {
d: 'piyo',
},
e: [5, 6, 7],
};
console.log(JSON.stringify(hoge));
/// 結果
{"a":1,"b":"fuga","c":{"d":"piyo"},"e":[5,6,7]}
参考までに、第3引数に数字を設定すると、その分だけインデントして出力してくれます。
let hoge = {
a: 1,
b: 'fuga',
c: {
d: 'piyo',
},
e: [5, 6, 7],
};
console.log(JSON.stringify(hoge, null, 2));
/// 結果
{
"a": 1,
"b": "fuga",
"c": {
"d": "piyo"
},
"e": [
5,
6,
7
]
}
JSON.stringifyの第2引数
第2引数は、出力を制御するために配列または関数を受け取ります。nullを指定した場合は、何も制御しないことになります。
第2引数が配列の場合
配列の場合は、その要素をキーとするプロパティだけが出力されるようになります。
let hoge = {
a: 'b',
c: 'd',
e: 'f',
};
console.log(JSON.stringify(hoge, ['a', 'e'], 2));
/// 結果
{
"a": "b",
"e": "f"
}
キーが数値なら、配列の要素は数値で指定することも可能です。
let hoge = {
1: 10,
2: 20,
3: 30,
};
console.log(JSON.stringify(hoge, [1, 3], 2));
/// 結果
{
"1": 10,
"3": 30
}
※JSONなので、出力結果のキー部分は二重引用符で囲まれた文字列扱いになります。
ネストされたオブジェクトの場合、指定されたキーに合致するなら末端まで処理されます。
let hoge = {
1: 10,
2: 20,
3: 30,
4: {
1: 110,
2: 120,
3: 130,
},
5: {
1: 210,
2: 220,
3: 230,
},
};
console.log(JSON.stringify(hoge, [1, 3, 4], 2));
/// 結果
{
"1": 10,
"3": 30,
"4": {
"1": 110,
"3": 130
}
}
第2引数が関数の場合
関数を指定すると、それぞれのプロパティに対して文字列化するための処理として適用されます。関数はkey, value
を受け取り、undefined
やnull
を返すプロパティは出力されなくなります。例えば、「key
が奇数の場合は出力しない」というような条件を、以下のように記述することができます。
let hoge = {
1: 10,
2: 20,
3: 30,
4: 40,
};
console.log(JSON.stringify(hoge, (key, value) => {
if (key % 2 !== 0) {
return undefined;
} else {
return value;
}
}, 2));
/// 結果
{
"2": 20,
"4": 40
}
それならばと、「key
が偶数の場合は出力しない」という条件で、以下のように記述すると、うまくいきません。
let hoge = {
1: 10,
2: 20,
3: 30,
4: 40,
};
console.log(JSON.stringify(hoge, (key, value) => {
if (key % 2 === 0) {
return undefined;
} else {
return value;
}
}, 2));
/// 結果
undefined
実は、「それぞれのプロパティ」として処理される要素の一番最初に、「key
=空文字、value
=変換対象のオブジェクト」というものがあって、このvalue
をいじってしまうと変換対象オブジェクトが変わってしまうようになっています。
試しに、処理される要素をコンソールに表示してみます。
let hoge = {
1: 10,
2: 20,
3: 30,
4: 40,
};
console.log(JSON.stringify(hoge, (key, value) => {
console.log('"' + key + '"', value);
if (key % 2 !== 0) {
return undefined;
} else {
return value;
}
}, 2));
/// 結果
"" {1: 10, 2: 20, 3: 30, 4: 40}
"1" 10
"2" 20
"3" 30
"4" 40
{
"2": 20,
"4": 40
}
このように、まず変換されるオブジェクト自身が最初の列挙対象になっています。前述の「key
が偶数の場合は出力しない」のケースでは、key
が空文字の場合にkey % 2 === 0
という条件に合致してしまい、変換されるオブジェクトがundefined
になってしまったのです。
※空文字は数式内では0
扱いになるため
したがって、以下のように関数内で第1引数とは全く別のオブジェクトにしてしまうことも可能です(使いどころはありませんが)。key === ''
以降は、新しいオブジェクトを対象として処理されています。
let hoge = {
a: 'b',
c: 'd',
e: 'f',
};
console.log(JSON.stringify(hoge, (key, value) => {
if (key === '') {
return {
1: 10,
2: 20,
3: 30,
4: 40,
};
} else if (key % 2 !== 0) {
return undefined;
} else {
return value;
}
}, 2));
/// 結果
{
"2": 20,
"4": 40
}
こういった事情があるため、例えば「元のオブジェクトの値部分に文字列を追加する」といった処理をしたい場合、「一番最初の処理」はスキップさせる(そのままvalue
を返す)必要があります。
let hoge = {
a: 'b',
c: 'd',
e: 'f',
'': 'g',
};
let isFirst = true;
console.log(JSON.stringify(hoge, (key, value) => {
if (isFirst) {
isFirst = false;
return value;
} else {
return value + '_add';
}
}, 2));
/// 結果
{
"a": "b_add",
"c": "d_add",
"e": "f_add",
"": "g_add"
}
あるいは、value
がobject
ならそのまま返す、というやり方もあります。ネストされたオブジェクトを扱うケースを考えると、そのほうがよいかもしれません。
let hoge = {
a: 'b',
c: 'd',
e: {
f: 'g',
h: 'i',
},
};
console.log(JSON.stringify(hoge, (key, value) => {
if (typeof value === 'object') {
// hoge自身の場合とhoge.eの場合にここを通る
return value;
} else {
return value + '_add';
}
}, 2));
/// 結果
{
"a": "b_add",
"c": "d_add",
"e": {
"f": "g_add",
"h": "i_add"
}
}
TIPS: 関数要素を扱うケース
要素が関数の場合にJSON.stringify
は要素を無視します。
let hoge = {
a: () => 'b',
c: 'd',
e: 'f',
};
console.log(JSON.stringify(hoge, null, 2));
/// 結果
{
"c": "d",
"e": "f"
}
関数はtoString()
で文字列化することが可能なので、以下のようにすれば関数部分も残すことができます。
let hoge = {
a: () => 'b',
c: 'd',
e: 'f',
};
console.log(JSON.stringify(hoge, (key, value) => {
if (typeof value === 'function') {
return value.toString();
} else {
return value;
}
}, 2));
/// 結果
{
"a": "() => 'b'",
"c": "d",
"e": "f"
}
TIPS: 循環参照を扱うケース
循環参照を含むオブジェクトについてJSON.stringify
を適用すると、下記のようにエラーが発生します。
let hoge = {
a: 'b',
};
hoge.c = hoge; // 循環参照
console.log(JSON.stringify(hoge));
/// 結果
Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'c' closes the circle
at JSON.stringify (<anonymous>)
これについては、本質的に循環参照をJSONで記述することは不可能であるため、代替文字を利用する等の対応をすることになります。
let hoge = {
a: 'b',
};
hoge.c = hoge; // 循環参照
hoge.d = {};
hoge.d.e = hoge.d; // 循環参照
let list = [];
console.log(JSON.stringify(hoge, (key, value) => {
if (typeof value === 'object') {
if (list.indexOf(value) > -1) {
return '$'; // 循環参照が現れたら'$'で表す
}
list.push(value);
}
return value;
}, 2));
/// 結果
{
"a": "b",
"c": "$",
"d": {
"e": "$"
}
}
ちなみに、これにしっかり対応したライブラリcycle.js
というのがあります。decycle
、retrocycle
という関数を導入しています。
let hoge = {
a: 'b',
};
hoge.c = hoge; // 循環参照
hoge.d = {};
hoge.d.e = hoge.d; // 循環参照
// stringify(JSON化する)
let fuga = JSON.stringify(JSON.decycle(hoge), null, 2);
console.log(fuga);
// parse(もとにもどす)
let piyo = JSON.retrocycle(JSON.parse(fuga));
console.log(_.isEqual(hoge, piyo)); // lodashを導入してオブジェクトの比較
/// 結果
{
"a": "b",
"c": {
"$ref": "$"
},
"d": {
"e": {
"$ref": "$[\"d\"]"
}
}
}
true
lodash.js
- オブジェクトの比較ができるライブラリ。べんり。
まとめ
以上、JSON.stringify
で情報を加工したい、関数の情報を残したい、循環参照があってもエラーにならないようにしたい、などという場合には第2引数をいじろう、というお話でした。