タグ別アーカイブ: JavaScript

JSON.stringify() / JSON.parse() で Map / Set 等を扱う

結論

第二引数に、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
    })
}

おわり

なんだかライブラリがありそうな気もしますが、これくらいなら必要な部分だけ自前で実装してもいいかなぁと感じます。