How do you convert a JavaScript Map to a plain object and back, and what does `JSON.stringify(new Map([['a', 1]]))` produce?
answer
- a Map is already an iterable of pairs
- one call each way
- JSON only sees own properties
- spread it into an array first
- keys become property names on the way out
basics
~10 sObject.fromEntries(map) builds a plain object from a Map, and new Map(Object.entries(obj)) converts back. JSON.stringify on a Map produces '{}' because its entries live in internal storage, not in own enumerable properties.
solid answer
~40 s`Object.fromEntries(map)` takes any iterable of `[key, value]` pairs, and a `Map` is exactly that, so it converts in one call; `new Map(Object.entries(obj))` goes the other way. `JSON.stringify` on a Map returns `'{}'` — it serializes own enumerable properties, and a Map's entries are held in internal storage that JSON knows nothing about, so the data disappears silently rather than erroring. To send a Map over the wire, serialize the pair array instead: `JSON.stringify([...map])` gives `[["a",1]]`, and `new Map(JSON.parse(text))` revives it. Note that the object round-trip is lossy when keys are not strings: `Object.fromEntries` converts each key to a property key, so the number `1` becomes `'1'` and two object keys can collapse onto one entry. `structuredClone`, unlike JSON, does preserve a Map faithfully.
code
javascript · 13 linesconst m = new Map([['a', 1], ['b', 2]]);
console.log(JSON.stringify(m)); // '{}' — entries are invisible to JSON
console.log(JSON.stringify([...m])); // '[["a",1],["b",2]]'
const obj = Object.fromEntries(m); // { a: 1, b: 2 }
const back = new Map(Object.entries(obj));
console.log(back.get('b')); // 2
const revived = new Map(JSON.parse('[["a",1],["b",2]]'));
console.log(revived.size); // 2
console.log(Object.keys(Object.fromEntries(new Map([[1, 'x']])))); // [ '1' ]go deeper
Remember the two conversions — Object.fromEntries(map) out and new Map(Object.entries(obj)) back — and that JSON.stringify on a Map gives an empty object rather than the entries.
Explain why JSON produces '{}': stringify walks own enumerable properties and a Map keeps entries in internal storage. Then show the pair-array workaround with spread and new Map(JSON.parse(text)).
Point out that the failure is silent, so it surfaces as empty caches or truncated payloads, and note where the object round-trip loses data — non-string keys coerced or collapsed — and that structuredClone is the correct in-memory clone.
Decide the serialization format once at the boundary rather than per call site: pick pair arrays or plain objects for the wire, keep Map internal, and make the conversion a single reviewed helper so no path stringifies a Map by accident.
## The two conversions A `Map` is an iterable of `[key, value]` pairs, and `Object.entries` produces exactly that shape from an object, so the two directions are one call each: ```js const m = new Map([['a', 1], ['b', 2]]); const obj = Object.fromEntries(m); // { a: 1, b: 2 } const back = new Map(Object.entries(obj)); // Map(2) { 'a' => 1, 'b' => 2 } ``` `Object.fromEntries` (ES2019) accepts *any* iterable of two-element entries, not just a Map, so `Object.fromEntries(pairsArray)` and `Object.fromEntries(url.searchParams)` work the same way. The `Map` constructor is equally liberal: it accepts any iterable of pairs, which is why `new Map(Object.entries(o))` and `new Map(arrayOfPairs)` both work. ## Why JSON.stringify gives `{}` `JSON.stringify` walks an object's own enumerable string-keyed properties. A `Map` has none — its entries live in internal storage reachable only through the Map methods — so the serializer finds an object with nothing to write and emits `'{}'`: ```js JSON.stringify(new Map([['a', 1]])); // '{}' JSON.stringify(new Set([1, 2])); // '{}' — same reason ``` Nothing throws. That silence is what makes it a real production bug: a cache written as JSON, a response body, or a `localStorage` entry becomes an empty object and the loss is only noticed downstream. ## Serializing a Map on purpose Spread the Map into its pair array first, and rebuild from that array on the way in: ```js const m = new Map([['a', 1], ['b', 2]]); const text = JSON.stringify([...m]); // '[["a",1],["b",2]]' const revived = new Map(JSON.parse(text)); // Map(2) { 'a' => 1, 'b' => 2 } ``` This preserves insertion order and, unlike the object route, keeps *string* keys distinct from each other without any coercion surprises — although JSON itself can only carry JSON-representable keys, so numeric keys still come back as numbers only because they were stored inside an array, not as property names. If you would rather the Map serialize itself, give the surrounding value a `toJSON` method, since `JSON.stringify` calls `toJSON` on any value that has one. Defining it on a wrapper object you own is safer than patching `Map.prototype`, which is a global change other code may not expect. Outside JSON, `structuredClone` (available in modern browsers and Node) understands `Map` and `Set` natively and returns a real `Map` with the same entries, including object keys — cloned, not shared. That is the right tool for cloning in memory or posting to a worker; JSON is the wrong tool for anything holding a Map. ## Where the object round-trip loses data `Object.fromEntries` converts each key with the ordinary property-key rules, so anything that is not a string or symbol becomes a string: ```js Object.keys(Object.fromEntries(new Map([[1, 'a']]))); // [ '1' ] — the number became a string const a = { id: 1 }; const b = { id: 2 }; Object.fromEntries(new Map([[a, 'x'], [b, 'y']])); // { '[object Object]': 'y' } — two entries collapsed into one ``` So the object route is lossless only when the keys are already strings. If a Map is keyed by objects or by numbers you care about, convert to the pair array instead, or map the keys to stable ids yourself before converting. Order is preserved in the friendly cases but not guaranteed to survive: a Map iterates in insertion order for all keys, whereas the resulting object visits integer-like keys first in ascending numeric order. A Map keyed `'10'`, `'2'`, `'x'` converts to an object whose keys enumerate as `'2'`, `'10'`, `'x'`. ## What to say in an interview Lead with the two one-liners, then the `'{}'` result and *why* it happens — own enumerable properties versus internal storage — then the pair-array workaround. Mentioning that the conversion is lossy for non-string keys, and that `structuredClone` handles Maps while JSON does not, is what separates a memorised answer from an understood one.
- Why does `{ ...map }` not produce an object with the Map's entries?Object spread copies own enumerable properties, and a Map has none — its entries sit in internal storage. So `{ ...map }` is `{}`, exactly like `Object.assign({}, map)` and `JSON.stringify(map)`. Array spread `[...map]` works because it uses the iteration protocol, which a Map does implement, yielding `[key, value]` pairs.
- If you need a Map to survive JSON, would you add a toJSON method to Map.prototype?I would avoid it. Patching a built-in prototype changes behaviour for every Map in the process, including library code that expects the default, and it is invisible at the call sites that depend on it. I would put the conversion in an explicit helper, or define `toJSON` on a wrapper type I own, so the transformation is local and reviewable.
- When is `Object.fromEntries` the wrong conversion for a Map?When the keys are not strings. Each key is converted to a property key, so numbers become their string form and any two objects collapse onto `'[object Object]'`, silently dropping entries. It also reorders integer-like keys ahead of the rest. For those Maps I serialize the pair array, or derive stable string ids for the keys first.
saying these in an interview costs you the question
- Says JSON.stringify on a Map throws an error
- Thinks {...map} copies the Map's entries into an object
- Claims Object.assign({}, map) converts a Map
- Assumes the object round-trip preserves non-string keys
- Believes structuredClone degrades a Map to a plain object