CLOVER🍀

That was when it all began.

JavaScriptのMapやSetをJSONやObjectあたりず盞互倉換したいずいう話

これは、なにをしたくお曞いたもの

JavaScriptのMapやSetをJSON#stringifyでJSON文字列にするず、思わぬ結果になったのでこのあたりをちょっずメモしおおこうかなず。

こんな感じですね。

// Map
test('apply JSON.stringify to Map', () => {
  const map = new Map([
    ['one', 1],
    ['two', 2],
    ['three', 3],
  ]);

  expect(JSON.stringify(map)).toBe('{}');
});


// Set
test('apply JSON.strinfigy to Set', () => {
  const set = new Set(['JavaScript', 'TypeScript', 'Node.js']);

  expect(JSON.stringify(set)).toBe('{}');
});

MapずObject、SetずArray

最初に曞いたように、MapやSetをJSON文字列にしようずするず、初芋ではちょっず驚く結果になりたす。

// Map
test('apply JSON.stringify to Map', () => {
  const map = new Map([
    ['one', 1],
    ['two', 2],
    ['three', 3],
  ]);

  expect(JSON.stringify(map)).toBe('{}');
});


// Set
test('apply JSON.strinfigy to Set', () => {
  const set = new Set(['JavaScript', 'TypeScript', 'Node.js']);

  expect(JSON.stringify(set)).toBe('{}');
});

倀がなにも入らずに{}になりたしたね 。

Map

たずMapから芋おいきたしょう。

Map - JavaScript | MDN

そもそもJSON#stringifyでのシリアラむズやJSON#parseでのパヌスに察応しおいないよ、ずMDNのMapのペヌゞに曞いおありたした。

シリアラむズや解釈のためのネむティブな察応はありたせん。

ただし、 replacer 匕数で JSON.stringify() を䜿甚し、 reviver 匕数で JSON.parse() を䜿甚するこずで、 Map のために、独自のシリアラむズず解釈の察応を䜜成するこずができたす。 Stack Overflow の質問 How do you JSON.stringify an ES6 Map? を参照しおください。

Map / 解説 / Object ず Map の比范

Mapのシリアラむズに぀いおはJSON#stringifyのreplacer匕数、パヌスにはJSON#parseのreviver匕数を䜿うず良い、ず曞かれお
いるのですが、もう少し手段があるようです。

Object#fromEntriesを䜿っお、1床Objectに倉換すればよいみたいです。Object#fromEntriesは、iterableなオブゞェクトをキヌず倀の
ペアからなる配列をオブゞェクトに倉換したす。
Mapはiterableです。

Object.fromEntries() - JavaScript | MDN

ずいうか、その甚途そのものが曞いおいたすね。

Object.fromEntries() / 䟋 / Map から Object ぞの倉換

Objectに倉換した埌であれば、JSON#stringifyでJSON文字列にできたす。 圓たり前ずいえば、圓たり前ですが。

JSON#parseの時はどうしたらいいかずいうず、Object#entriesでオブゞェクトをキヌず倀のペアの配列に倉換したす。

Object.entries() - JavaScript | MDN

こちらも、その甚途そのものが曞いおいたす。

Object.entries() / 䟋 / Object から Map ぞの倉換

こうすれば、あずはMapのコンストラクタヌに枡せばMapずしお構成できたす。

Map() コンストラクター - JavaScript | MDN

Objectを1床経由するこずで、MapずJSON文字列の倉換を行うこずになりたす。

参考
- Map<K, V> / Mapは盎接JSONにできない - Map<K, V> / 他の型ずの盞互運甚

Set

Setにはシリアラむズに぀いおは特に曞かれおいたせん。

Set - JavaScript | MDN

ですが、理屈はMapず䌌たようなもので、Setに぀いおは1床Arrayを間に挟めばよさそうです。

Setはiterableですし、コンストラクタヌで配列を受け取っお配列からSetに倉換するこずもできたす。

Set() コンストラクター - JavaScript | MDN

Setから配列ぞの倉換は、スプレッド挔算子を䜿うず簡単そうですね。

スプレッド構文 - JavaScript | MDN

参考
- Set / Setの操䜜 / Setを配列に倉換する - Set / Setは盎接JSONにできない

冷静に考えるずMapもSetもJSONの衚珟の範囲倖なので盎接倉換しようずしおもムリなのはそうですね、間にJSONで衚珟可胜なものを
挟めばそりゃあできたすよね、ずいう感じなのですが。
特にMapがJSON#stringifyで倉換できないのは、なにも考えない状態で芋るず割ず驚く気がしたす。

ずいうわけで、簡単に詊しおみたす。Node.jsTypeScriptの環境で確認するこずにしたす。

環境

今回の環境は、こちら。

$ node --version
v18.15.0


$ npm --version
9.5.0

Node.jsプロゞェクトを䜜成する

Node.jsプロゞェクトを䜜成したす。䞀緒にTypeScript、テストコヌドでの確認ずいうこずでJestもむンストヌル。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v18
$ npm i -D prettier
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir test

䟝存関係。

  "devDependencies": {
    "@types/jest": "^29.5.0",
    "@types/node": "^18.15.5",
    "esbuild": "^0.17.12",
    "esbuild-jest": "^0.5.0",
    "jest": "^29.5.0",
    "prettier": "^2.8.6",
    "typescript": "^5.0.2"
  }

scripts。

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig.typecheck.json",
    "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
    "test": "jest",
    "format": "prettier --write src test"
  },

蚭定ファむル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

この環境で詊しおいきたしょう。

Map

たずはMapから。

そのたたJSON#stringify、JSON#parse

最初はなにも考えずにMapをJSON#stringifyでJSON文字列に倉換しおみたす。これは最初に芋たしたが、{}になりたす。

test('apply JSON.stringify to Map', () => {
  const map = new Map([
    ['one', 1],
    ['two', 2],
    ['three', 3],
  ]);

  expect(JSON.stringify(map)).toBe('{}');
});

たた、JSON#parseでムリダリMapにキャストしたずころで、実䜓はオブゞェクトなのでMapのむンスタンスではありたせん。

test('parse JSON as Map with JSON.parse', () => {
  const map = JSON.parse('{"one":1,"two":2,"three":3}') as Map<string, number>;

  expect(map).toEqual({ one: 1, two: 2, three: 3 });
  expect(map).not.toBeInstanceOf(Map);
  expect(() => map.has('one')).toThrow(new TypeError('map.has is not a function'));
});

Mapのメ゜ッドを䜿ったずころで、䟋倖がスロヌされたす。

Object#fromEntriesJSON#stringify、JSON#parseObject#entriesMapコンストラクタヌ

次は、MapずJSON文字列の倉換の間に、Object#fromEntriesやObject#entriesを挟んでみたす。

MapからJSON文字列ぞの倉換。

test('apply JSON.stringify to Object.fromEntries(Map)', () => {
  const map = new Map([
    ['one', 1],
    ['two', 2],
    ['three', 3],
  ]);

  expect(JSON.stringify(Object.fromEntries(map))).toBe('{"one":1,"two":2,"three":3}');
});

JSON#stringifyの前に、MapをObject#fromEntriesでオブゞェクトに倉換するこずでJSON文字列にシリアラむズできたした。

次はJSON文字列からMapぞの倉換。

test('parse JSON as Object with JSON.parse, convert to Map', () => {
  const map = new Map<string, number>(Object.entries(JSON.parse('{"one":1,"two":2,"three":3}')));

  expect(map).toEqual(
    new Map([
      ['one', 1],
      ['two', 2],
      ['three', 3],
    ])
  );
  expect(map).toBeInstanceOf(Map);
});

ここでは、JSON#parseの結果オブゞェクトをObject#entriesでキヌず倀のペアの配列に倉換しお、そこからMapに再構成しお
いたす。

これで、MapをJSON文字列に倉換したり、その逆ができるようになりたした。

Set

続いおはSet。

そのたたJSON#stringify、JSON#parse

こちらも、最初はなにも考えずにMapをJSON#stringifyでJSON文字列に倉換しおみたす。こちらも{}になりたす。

test('apply JSON.strinfigy to Set', () => {
  const set = new Set(['JavaScript', 'TypeScript', 'Node.js']);

  expect(JSON.stringify(set)).toBe('{}');
});

たたJSON#parseでムリダリSetにキャストしたずころで、実䜓は配列なのでSetのむンスタンスずしおは扱われたせん。

test('parse JSON as Set with JSON.parse', () => {
  const set = JSON.parse('["JavaScript","TypeScript","Node.js"]') as Set<string>;

  expect(set).toEqual(['JavaScript', 'TypeScript', 'Node.js']);
  expect(set).not.toBeInstanceOf(Set);
  expect(set).toBeInstanceOf(Array);
  expect(() => set.has('JavaScript')).toThrow(new TypeError('set.has is not a function'));
});
配列倉換JSON#stringify、JSON#parseSetコンストラクタヌ

では、SetずJSON文字列の間に1床明瀺的に配列ぞの倉換を挟むこずで、これらの問題を解消したいず思いたす。

たずはJSON#stringifyの前に、スプレッド挔算子ず組み合わせおSetを配列に倉換。

test('convert Set to Array, apply JSON.strinfigy', () => {
  const set = new Set(['JavaScript', 'TypeScript', 'Node.js']);

  expect(JSON.stringify([...set])).toBe('["JavaScript","TypeScript","Node.js"]');
});

配列をJSON文字列にしおいるので、配列のJSON文字列衚珟になりたしたね。

JSON#parseの堎合は、配列をSetに倉換すればよいので、そのたたSetのコンストラクタヌに枡したす。

test('parse JSON as Array with JSON.parse, convert to Set', () => {
  const set = new Set<string>(JSON.parse('["JavaScript","TypeScript","Node.js"]'));

  expect(set).toEqual(new Set(['JavaScript', 'TypeScript', 'Node.js']));
  expect(set).toBeInstanceOf(Set);
  expect(set).not.toBeInstanceOf(Array);
});

これで、配列を経由しおJSON文字列をSetに倉換できたした。

もう少し

最初にMapのドキュメントでシリアラむズに぀いお芋た時、以䞋のような蚘述がありたした。

シリアラむズや解釈のためのネむティブな察応はありたせん。

ただし、 replacer 匕数で JSON.stringify() を䜿甚し、 reviver 匕数で JSON.parse() を䜿甚するこずで、 Map のために、独自のシリアラむズず解釈の察応を䜜成するこずができたす。 Stack Overflow の質問 How do you JSON.stringify an ES6 Map? を参照しおください。

Map / 解説 / Object ず Map の比范

ここで曞かれおいるStack Overflowの質問は、以䞋になりたす。

javascript - How do you JSON.stringify an ES6 Map? - Stack Overflow

それぞれ以䞋のような関数を䜜成し、シリアラむズ、パヌス凊理をカスタマむズしようずいう話です。

// JSON#stringifyで䜿う
function replacer(key, value) {
  if(value instanceof Map) {
    return {
      dataType: 'Map',
      value: Array.from(value.entries()), // or with spread: value: [...value]
    };
  } else {
    return value;
  }
}


// JSON#parseで䜿う
function reviver(key, value) {
  if(typeof value === 'object' && value !== null) {
    if (value.dataType === 'Map') {
      return new Map(value.value);
    }
  }
  return value;
}

この関数を䜿っおMapをシリアラむズするず、以䞋のように型情報を埋め蟌んだ圢になりたす。

{
  "dataType": "Map",
  "value": [[key, value],[key,value],...]
}

パヌスの時には、この型情報を芋おMapに戻そうずするわけですね。

JSON#stringifyにおけるreplacer関数はオプションの匕数で、JSON文字列に倉換する際の挙動をカスタマむズできたす。

JSON.stringify() - JavaScript | MDN

JSON#parseにおけるreviver関数も同じくオプションの匕数で、JSON文字列からオブゞェクトに倉換する際の凊理をカスタマむズ
できたす。

JSON.parse() - JavaScript | MDN

このあたりを䜿わなくおも、先ほどたでに曞いたObject#fromEntriesなどを䜿えばいいのではずいう気がしたすが、たずえば
JSON文字列化する察象のオブゞェクトのプロパティにMapがあったりするず、Object#fromEntriesではうたくいきたせん。
これで効果があるのは、トップレベルのオブゞェクトがMapだった堎合ですからね。

ずいうわけで、今回はMapで簡単に詊すこずにしたす。

たずはJSON#stringfyreplacer関数。

test('apply JSON.stringify to Object include Map', () => {
  const book = {
    isbn: '978-4873119700',
    title: 'JavaScript 第7版',
    price: 5060,
    tags: new Map([
      ['language', 'JavaScript'],
      ['category', 'Programming'],
    ]),
  };

  expect(JSON.stringify(book)).toBe('{"isbn":"978-4873119700","title":"JavaScript 第7版","price":5060,"tags":{}}');

  const replacer = (key: any, value: any) => {
    if (value instanceof Map) {
      return Object.fromEntries(value);
    }

    return value;
  };

  expect(JSON.stringify(book, replacer)).toBe(
    '{"isbn":"978-4873119700","title":"JavaScript 第7版","price":5060,"tags":{"language":"JavaScript","category":"Programming"}}'
  );
});

こんな感じで、Mapをプロパティに持぀オブゞェクトでも問題なくJSON文字列にできたした。今回は、型情報をJSONに埋め蟌むこずは
しおいたせん。

次はJSON#parsereviver関数。

test('parse JSON as Object with JSON.parse, convert to Object include Map', () => {
  const bookString =
    '{"isbn":"978-4873119700","title":"JavaScript 第7版","price":5060,"tags":{"language":"JavaScript","category":"Programming"}}';

  const bookAsObject = JSON.parse(bookString);
  expect(bookAsObject.tags).not.toBeInstanceOf(Map);
  expect(bookAsObject).not.toEqual({
    isbn: '978-4873119700',
    title: 'JavaScript 第7版',
    price: 5060,
    tags: new Map([
      ['language', 'JavaScript'],
      ['category', 'Programming'],
    ]),
  });
  expect(bookAsObject).toEqual({
    isbn: '978-4873119700',
    title: 'JavaScript 第7版',
    price: 5060,
    tags: {
      language: 'JavaScript',
      category: 'Programming',
    },
  });

  const revivier = (key: any, value: any) => {
    if (key === 'tags') {
      return new Map(Object.entries(value));
    }

    return value;
  };

  const book = JSON.parse(bookString, revivier);
  expect(book).toEqual({
    isbn: '978-4873119700',
    title: 'JavaScript 第7版',
    price: 5060,
    tags: new Map([
      ['language', 'JavaScript'],
      ['category', 'Programming'],
    ]),
  });
  expect(book.tags).toBeInstanceOf(Map);
});

この䟋では型情報がないので、プロパティ名で決め打ちでMapに倉換するようにしおいたす。

ちなみに、ここたで曞くずなんずなく気づくのですが、トップレベルのオブゞェクトがMapであっおもreplacer関数でシリアラむズや
reviver関数でパヌスするこずができたす。

こんな感じですね。

## JSON#stringifyreplacer
test('apply JSON.stringify with replacer to Object include Map', () => {
  const map = new Map([
    ['one', 1],
    ['two', 2],
    ['three', 3],
  ]);

  const replacer = (key: any, value: any) => {
    if (value instanceof Map) {
      expect(key).toBe('');
      return Object.fromEntries(value);
    }

    return value;
  };

  expect(JSON.stringify(map, replacer)).toBe('{"one":1,"two":2,"three":3}');
});


## JSON#parsereviver
test('parse JSON as Map with JSON.parse with revivier', () => {
  const mapAsString = '{"one":1,"two":2,"three":3}';

  const revivier = (key: any, value: any) => {
    if (key === '') {
      return new Map(Object.entries(value));
    }

    return value;
  };

  const map = JSON.parse(mapAsString, revivier);
  expect(map).toEqual(
    new Map([
      ['one', 1],
      ['two', 2],
      ['three', 3],
    ])
  );
  expect(map).toBeInstanceOf(Map);
});

たあ、手間が増えるだけなのでやらないでしょうけどね 。

キヌがない堎合は、空文字列が枡されるようですね。

今回は、こんなずころで。

たずめ

MapやSetをJSON文字列にシリアラむズ、パヌスしようずしおハマったので、ちょっずたずめおおきたした。

察応方法はわかったのですが、ネストしたプロパティにMapやSetがいたりするず厄介なこずになるので、JSONに倉換するオブゞェクトは
玠盎にJSONで衚珟できる範囲にずどめた方がMapやSetを含めない方がいいのかなず思いたした 。