-
JSON.stringify() / JSON.parse() で Map / Set 等を扱う
- 2021年10月15日
- ECMAScript
- JavaScript
- JSON
結論
第二引数に、key / valueを引数にとる変換関数を書けばOKです。
stringify ではプレーンオブジェクト等へ変換する replacer
、
parse ではもとのMapやSetなどに戻す reviver
を渡します。
サンプルコード
Mapを扱う場合
const someObject = {
someKey: "value",
map: new Map([[1, "a"], [2, "b"]])
}
// stringify
const json = JSON.stringify(someObject, (k, v) => {
if (v instanceof Map) {
return {
dataType: "Map",
value: [...v]
}
}
return v
})
console.log(json) // '{"someKey":"value","map":{"dataType":"Map","value":[[1,"a"],[2,"b"]]}}'
// parse
const parsedObject = JSON.parse(json, (k, v) => {
if (typeof v === "object" && v !== null) {
if (v.dataType === "Map") {
return new Map(v.value)
}
}
return v
})
console.log(parsedObject) // { someKey: 'value', map: Map(2) { 1 => 'a', 2 => 'b' } }
本文
モチベーション
ES6からMap, Set等の便利なデータストラクチャーが追加されましたが、これらは JSON.stringify()
で変換することができません(JSONの仕様にないからですね)。
JSON.stringify({someMap: new Map([[1,"a"]])})
// '{"someMap":{}}'
比較的新しいものだとBigIntとかもだめですね(これはプリミティブですが)。こっちはエラーを投げてきます。
JSON.stringify({someBigInt: 11n})
// Uncaught TypeError: Do not know how to serialize a BigInt
// at JSON.stringify (<anonymous>)
とはいえ、これらをデシリアライズしたい場面は存在します。
たとえば、データの保存とかエクスポートとか。
変換方法を定義できる第二引数が用意されているので、それを使うと実現できます。
方針
この第二引数を、以下のような方針で使うことにします。
JSON.stringify()
では、JSONとして記述できるプレーンオブジェクトに変換するJSON.parse()
では、元々のオブジェクト等に変換する
JSON.stringify() の第二引数
みんなだいすきMDN。
JSON.stringify() – JavaScript | MDN
replacer 引数は関数または配列です。
今回の用途では関数を使います。
オブジェクトを返した場合、そのオブジェクトはそれぞれのプロパティに replacer 関数を呼び出して再帰的に文字列化します。
要は、この関数は再帰的に適用されるということです。たすかる。
サンプルを以下に示します。Map以外が必要だったら、同様に記述すればOKです。
const replacer = (k, v) => { // key, valueを受け取る
if (v instanceof Map) { // valueがMapのインスタンスだったら……
return { // 独自に定義したオブジェクトに変換して返す
dataType: "Map",
value: [...v] // MapはIterable。Array.from(v)もOK
}
}
return v // それ以外は標準のまま返す(これをしないと消える)
}
そして、実際に使います。
JSON.stringify(someObject, replacer)
これにより、Mapは以下のようにプレーンなオブジェクトに変換された上で文字列化されます(読みやすいようフォーマットしていますが、デフォルトではminifyされているはずです)。
{
"dataType": "Map",
"value": [
[1, "a"],
[2, "b"]
]
}
JSON.parse() の第二引数
先の結果を JSON.parse()
に渡しても、当然ながらMapに戻りません。
JSON.parse(
'{"map":{"dataType":"Map","value":[[1,"a"],[2,"b"]]}}'
)
// {
// map: { dataType: 'Map', value: [ [Array], [Array] ] } // plain object!
// }
というわけでMDN。
JSON.parse() – JavaScript | MDN
reviver
省略可もし関数である場合、解析により作り出された元の値を、オブジェクトを返す前に変換する方法を指示します。
要は、変換関数を定義して渡せるよ、ということです。
サンプルを以下に示します。Map以外が必要だったら、同様に記述すればOKです。
const reviver = (k, v) => { // key, valueを受け取る
if (typeof v === "object" && v !== null) { // valueがnull以外のObjectで
if (v.dataType === "Map") { // dataTypeプロパティが "Map" だった場合(先ほど独自に定義した箇所ですね)
return new Map(v.value) // Mapにして返す
}
}
return v // それ以外は標準のまま返す(これをしないと消える)
}
そして、実際に使います。
JSON.parse(
'{"map":{"dataType":"Map","value":[[1,"a"],[2,"b"]]}}',
reviver
)
ちゃんとMapになりました。
// { map: Map(2) { 1 => 'a', 2 => 'b' } }
実用
いちいち渡すのは煩雑なので、ラップして使うのがよさそうです。
export const jsonStringify = (obj) => {
return JSON.stringify(someObject, (k, v) => {
if (v instanceof Map) {
return {
dataType: "Map",
value: [...v]
}
}
return v
})
}
export const jsonParse = (obj) => {
JSON.parse(json, (k, v) => {
if (typeof v === "object" && v !== null) {
if (v.dataType === "Map") {
return new Map(v.value)
}
}
return v
})
}
おわり
なんだかライブラリがありそうな気もしますが、これくらいなら必要な部分だけ自前で実装してもいいかなぁと感じます。
この記事を書いた人 : 池上龍一
AWS bluebird css CSV docker docker compose electron ES6 es2015 Git Heroku ITコンサルティング JavaScript justinmind less mongoDB Node.js php PostgreSQL Private Space Promise React react-router reactjs Salesforce scss Selenium Builder selenium IDE Selenium WebDriver stylus TypeScript VirtualBox VisualStudioCode vue vuejs webpack システム開発プロジェクト セキュリティ ワイヤーフレーム 上流工程 卒FIT 帳票 要件定義 設計 電力小売業界