How to use javascript proxy for nested objects - javascript

I have this code in js bin:
var validator = {
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
if(isObject(target[key])){
}
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
if i do proxy.inner.salary = 555; it does not work.
However if i do proxy.firstName = "Anne", then it works great.
I do not understand why it does not work Recursively.
http://jsbin.com/dinerotiwe/edit?html,js,console

You can add a get trap and return a new proxy with validator as a handler:
var validator = {
get(target, key) {
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], validator)
} else {
return target[key];
}
},
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'

A slight modification on the example by Michał Perłakowski with the benefit of this approach being that the nested proxy is only created once rather than every time a value is accessed.
If the property of the proxy being accessed is an object or array, the value of the property is replaced with another proxy. The isProxy property in the getter is used to detect whether the currently accessed object is a proxy or not. You may want to change the name of isProxy to avoid naming collisions with properties of stored objects.
Note: the nested proxy is defined in the getter rather than the setter so it is only created if the data is actually used somewhere. This may or may not suit your use-case.
const handler = {
get(target, key) {
if (key == 'isProxy')
return true;
const prop = target[key];
// return if property not found
if (typeof prop == 'undefined')
return;
// set value as proxy if object
if (!prop.isProxy && typeof prop === 'object')
target[key] = new Proxy(prop, handler);
return target[key];
},
set(target, key, value) {
console.log('Setting', target, `.${key} to equal`, value);
// todo : call callback
target[key] = value;
return true;
}
};
const test = {
string: "data",
number: 231321,
object: {
string: "data",
number: 32434
},
array: [
1, 2, 3, 4, 5
],
};
const proxy = new Proxy(test, handler);
console.log(proxy);
console.log(proxy.string); // "data"
proxy.string = "Hello";
console.log(proxy.string); // "Hello"
console.log(proxy.object); // { "string": "data", "number": 32434 }
proxy.object.string = "World";
console.log(proxy.object.string); // "World"

I published a library on GitHub that does this as well. It will also report to a callback function what modifications have taken place along with their full path.
Michal's answer is good, but it creates a new Proxy every time a nested object is accessed. Depending on your usage, that could lead to a very large memory overhead.

I have also created a library type function for observing updates on deeply nested proxy objects (I created it for use as a one-way bound data model). Compared to Elliot's library it's slightly easier to understand at < 100 lines. Moreover, I think Elliot's worry about new Proxy objects being made is a premature optimisation, so I kept that feature to make it simpler to reason about the function of the code.
observable-model.js
let ObservableModel = (function () {
/*
* observableValidation: This is a validation handler for the observable model construct.
* It allows objects to be created with deeply nested object hierarchies, each of which
* is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
* <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
* <rootTarget> the earliest property in this <path> which contained an observers array *
*/
let observableValidation = {
get(target, prop) {
this.updateMarkers(target, prop);
if (target[prop] && typeof target[prop] === 'object') {
target[prop] = new Proxy(target[prop], observableValidation);
return new Proxy(target[prop], observableValidation);
} else {
return target[prop];
}
},
set(target, prop, value) {
this.updateMarkers(target, prop);
// user is attempting to update an entire observable field
// so maintain the observers array
target[prop] = this.path.length === 1 && prop !== 'length'
? Object.assign(value, { observers: target[prop].observers })
: value;
// don't send events on observer changes / magic length changes
if(!this.path.includes('observers') && prop !== 'length') {
this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
}
// reset the markers
this.rootTarget = undefined;
this.path.length = 0;
return true;
},
updateMarkers(target, prop) {
this.path.push(prop);
this.rootTarget = this.path.length === 1 && prop !== 'length'
? target[prop]
: target;
},
path: [],
set rootTarget(target) {
if(typeof target === 'undefined') {
this._rootTarget = undefined;
}
else if(!this._rootTarget && target.hasOwnProperty('observers')) {
this._rootTarget = Object.assign({}, target);
}
},
get rootTarget() {
return this._rootTarget;
}
};
/*
* create: Creates an object with keys governed by the fields array
* The value at each key is an object with an observers array
*/
function create(fields) {
let observableModel = {};
fields.forEach(f => observableModel[f] = { observers: [] });
return new Proxy(observableModel, observableValidation);
}
return {create: create};
})();
It's then trivial to create an observable model and register observers:
app.js
// give the create function a list of fields to convert into observables
let model = ObservableModel.create([
'profile',
'availableGames'
]);
// define the observer handler. it must have an onEvent function
// to handle events sent by the model
let profileObserver = {
onEvent(field, newValue) {
console.log(
'handling profile event: \n\tfield: %s\n\tnewValue: %s',
JSON.stringify(field),
JSON.stringify(newValue));
}
};
// register the observer on the profile field of the model
model.profile.observers.push(profileObserver);
// make a change to profile - the observer prints:
// handling profile event:
// field: ["profile"]
// newValue: {"name":{"first":"foo","last":"bar"},"observers":[{}
// ]}
model.profile = {name: {first: 'foo', last: 'bar'}};
// make a change to available games - no listeners are registered, so all
// it does is change the model, nothing else
model.availableGames['1234'] = {players: []};
Hope this is useful!

I wrote a function based on Michał Perłakowski code. I added access to the path of property in the set/get functions. Also, I added types.
const createHander = <T>(path: string[] = []) => ({
get: (target: T, key: keyof T): any => {
if (key == 'isProxy') return true;
if (typeof target[key] === 'object' && target[key] != null)
return new Proxy(
target[key],
createHander<any>([...path, key as string])
);
return target[key];
},
set: (target: T, key: keyof T, value: any) => {
console.log(`Setting ${[...path, key]} to: `, value);
target[key] = value;
return true;
}
});
const proxy = new Proxy(obj ,createHander<ObjectType>());

Related

Get path when accessing a deep object

I have created deep proxy object. Now I want that whenever someone access a deep property it must show the path.
const deepPath = [];
function deepProxy(obj, path = []) {
const store = {};
return new Proxy(obj, {
deleteProperty(obj, prop) {
delete obj[prop];
delete store[prop];
return true;
},
get(obj, prop) {
if (store[prop]) return store[prop];
const value = obj[prop];
if (isObject(value))
return (store[prop] = deepProxy(value, [...path, prop]));
return value;
},
set(obj, prop, value) {
if (isObject(value))
value = store[prop] = deepProxy(value, [...path, prop]);
obj[prop] = value;
return true;
}
});
}
const obj = deepProxy({
first: {
second: {
deep: true
}
}
});
Now it is deep proxy. Now if someone access a deep object or value. then how i can know path.
for exaMPLE. Whenever i access a value it must push full path into "deepPath" array.
console.log(obj.first.second);
// should push "first.second"
console.log(obj.first);
// should push "first"
console.log(obj.first.second.deep);
// should push "first.second.deep"
Is there any way to do this ?

Promise.all return value [duplicate]

I'd like to start using ES6 Map instead of JS objects but I'm being held back because I can't figure out how to JSON.stringify() a Map. My keys are guaranteed to be strings and my values will always be listed. Do I really have to write a wrapper method to serialize?
Both JSON.stringify and JSON.parse support a second argument. replacer and reviver respectively. With replacer and reviver below it's possible to add support for native Map object, including deeply nested values
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
Usage:
const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
Deep nesting with combination of Arrays, Objects and Maps
const originalValue = [
new Map([['a', {
b: {
c: new Map([['d', 'text']])
}
}]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
You can't directly stringify the Map instance as it doesn't have any properties, but you can convert it to an array of tuples:
jsonText = JSON.stringify(Array.from(map.entries()));
For the reverse, use
map = new Map(JSON.parse(jsonText));
You can't.
The keys of a map can be anything, including objects. But JSON syntax only allows strings as keys. So it's impossible in a general case.
My keys are guaranteed to be strings and my values will always be lists
In this case, you can use a plain object. It will have these advantages:
It will be able to be stringified to JSON.
It will work on older browsers.
It might be faster.
While there is no method provided by ecmascript yet, this can still be done using JSON.stingify if you map the Map to a JavaScript primitive. Here is the sample Map we'll use.
const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');
Going to an JavaScript Object
You can convert to JavaScript Object literal with the following helper function.
const mapToObj = m => {
return Array.from(m).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'
Going to a JavaScript Array of Objects
The helper function for this one would be even more compact
const mapToAoO = m => {
return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'
Going to Array of Arrays
This is even easier, you can just use
JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'
Using spread sytax Map can be serialized in one line:
JSON.stringify([...new Map()]);
and deserialize it with:
let map = new Map(JSON.parse(map));
Given your example is a simple use case in which keys are going to be simple types, I think this is the easiest way to JSON stringify a Map.
JSON.stringify(Object.fromEntries(map));
The way I think about the underlying data structure of a Map is as an array of key-value pairs (as arrays themselves). So, something like this:
const myMap = new Map([
["key1", "value1"],
["key2", "value2"],
["key3", "value3"]
]);
Because that underlying data structure is what we find in Object.entries, we can utilize the native JavaScript method of Object.fromEntries() on a Map as we would on an Array:
Object.fromEntries(myMap);
/*
{
key1: "value1",
key2: "value2",
key3: "value3"
}
*/
And then all you're left with is using JSON.stringify() on the result of that.
A Better Solution
// somewhere...
class Klass extends Map {
toJSON() {
var object = { };
for (let [key, value] of this) object[key] = value;
return object;
}
}
// somewhere else...
import { Klass as Map } from '#core/utilities/ds/map'; // <--wherever "somewhere" is
var map = new Map();
map.set('a', 1);
map.set('b', { datum: true });
map.set('c', [ 1,2,3 ]);
map.set( 'd', new Map([ ['e', true] ]) );
var json = JSON.stringify(map, null, '\t');
console.log('>', json);
Output
> {
"a": 1,
"b": {
"datum": true
},
"c": [
1,
2,
3
],
"d": {
"e": true
}
}
Hope that is less cringey than the answers above.
Stringify a Map instance (objects as keys are OK):
JSON.stringify([...map])
or
JSON.stringify(Array.from(map))
or
JSON.stringify(Array.from(map.entries()))
output format:
// [["key1","value1"],["key2","value2"]]
Below solution works even if you have nested Maps
function stringifyMap(myMap) {
function selfIterator(map) {
return Array.from(map).reduce((acc, [key, value]) => {
if (value instanceof Map) {
acc[key] = selfIterator(value);
} else {
acc[key] = value;
}
return acc;
}, {})
}
const res = selfIterator(myMap)
return JSON.stringify(res);
}
The very simple way.
const map = new Map();
map.set('Key1', "Value1");
map.set('Key2', "Value2");
console.log(Object.fromEntries(map));
`
Output:-
{"Key1": "Value1","Key2": "Value2"}
Just want to share my version for both Map and Set JSON.stringify only.
I'm sorting them, useful for debugging...
function replacer(key, value) {
if (value instanceof Map) {
const reducer = (obj, mapKey) => {
obj[mapKey] = value.get(mapKey);
return obj;
};
return [...value.keys()].sort().reduce(reducer, {});
} else if (value instanceof Set) {
return [...value].sort();
}
return value;
}
Usage:
const map = new Map();
const numbers= new Set()
numbers.add(3);
numbers.add(2);
numbers.add(3);
numbers.add(1);
const chars= new Set()
chars.add('b')
chars.add('a')
chars.add('a')
map.set("numbers",numbers)
map.set("chars",chars)
console.log(JSON.stringify(map, replacer, 2));
Result:
{
"chars": [
"a",
"b"
],
"numbers": [
1,
2,
3
]
}
You cannot call JSON.stringify on Map or Set.
You will need to convert:
the Map into a primitive Object, using Object.fromEntries, or
the Set into a primitive Array, using the spread operator [...]
…before calling JSON.stringify
Map
const
obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
map = new Map(Object.entries(obj));
map.set('Key3', 'Value3'); // Add a new entry
// Does NOT show the key-value pairs
console.log('Map:', JSON.stringify(map));
// Shows the key-value pairs
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Set
const
arr = ['Value1', 'Value2'],
set = new Set(arr);
set.add('Value3'); // Add a new item
// Does NOT show the values
console.log('Set:', JSON.stringify(set));
// Show the values
console.log(JSON.stringify([...set], null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
toJSON method
If you want to call JSON.stringify on a class object, you will need to override the toJSON method to return your instance data.
class Cat {
constructor(options = {}) {
this.name = options.name ?? '';
this.age = options.age ?? 0;
}
toString() {
return `[Cat name="${this.name}", age="${this.age}"]`
}
toJSON() {
return { name: this.name, age: this.age };
}
static fromObject(obj) {
const { name, age } = obj ?? {};
return new Cat({ name, age });
}
}
/*
* JSON Set adds the missing methods:
* - toJSON
* - toString
*/
class JSONSet extends Set {
constructor(values) {
super(values)
}
toString() {
return super
.toString()
.replace(']', ` ${[...this].map(v => v.toString())
.join(', ')}]`);
}
toJSON() {
return [...this];
}
}
const cats = new JSONSet([
Cat.fromObject({ name: 'Furball', age: 2 }),
Cat.fromObject({ name: 'Artemis', age: 5 })
]);
console.log(cats.toString());
console.log(JSON.stringify(cats, null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Correctly round-tripping serialization
Just copy this and use it. Or use the npm package.
const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);
// License: CC0
function stringifyReplacer(key, value) {
if (typeof value === "object" && value !== null) {
if (value instanceof Map) {
return {
_meta: { type: "map" },
value: Array.from(value.entries()),
};
} else if (value instanceof Set) { // bonus feature!
return {
_meta: { type: "set" },
value: Array.from(value.values()),
};
} else if ("_meta" in value) {
// Escape "_meta" properties
return {
...value,
_meta: {
type: "escaped-meta",
value: value["_meta"],
},
};
}
}
return value;
}
function parseReviver(key, value) {
if (typeof value === "object" && value !== null) {
if ("_meta" in value) {
if (value._meta.type === "map") {
return new Map(value.value);
} else if (value._meta.type === "set") {
return new Set(value.value);
} else if (value._meta.type === "escaped-meta") {
// Un-escape the "_meta" property
return {
...value,
_meta: value._meta.value,
};
} else {
console.warn("Unexpected meta", value._meta);
}
}
}
return value;
}
Why is this hard?
It should be possible to input any kind of data, get valid JSON, and from there correctly reconstruct the input.
This means dealing with
Maps that have objects as keys new Map([ [{cat:1}, "value"] ]). This means that any answer which uses Object.fromEntries is probably wrong.
Maps that have nested maps new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ]). A lot of answers sidestep this by only answering the question and not dealing with anything beyond that.
Mixing objects and maps {"key": new Map([ ["nested key", "nested value"] ]) }.
and on top of those difficulties, the serialisation format must be unambiguous. Otherwise one cannot always reconstruct the input. The top answer has one failing test case, see below.
Hence, I wrote this improved version. It uses _meta instead of dataType, to make conflicts rarer and if a conflict does happen, it actually unambiguously handles it. Hopefully the code is also simple enough to easily be extended to handle other containers.
My answer does, however, not attempt to handle exceedingly cursed cases, such as a map with object properties.
A test case for my answer, which demonstrates a few edge cases
const originalValue = [
new Map([['a', {
b: {
_meta: { __meta: "cat" },
c: new Map([['d', 'text']])
}
}]]),
{ _meta: { type: "map" }}
];
console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));
Accepted answer not round-tripping
The accepted answer is really lovely. However, it does not round trip when an object with a dataType property is passed it it.
// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue);
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different
I really don't know why there are so many long awesers here. This short version solved my problem:
const data = new Map()
data.set('visible', true)
data.set('child', new Map())
data.get('child').set('visible', false)
const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
// '{"visible":true,"child":{"visible":false}}'
const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
// Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }
The following method will convert a Map to a JSON string:
public static getJSONObj(): string {
return JSON.stringify(Object.fromEntries(map));
}
Example:
const x = new Map();
x.set("SomeBool", true);
x.set("number1", 1);
x.set("anObj", { name: "joe", age: 22, isAlive: true });
const json = getJSONObj(x);
// Output:
// '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'
Although there would be some scenarios where if you were the creator of the map you would write your code in a separate 'src' file and save a copy as a .txt file and, if written concisely enough, could easily be read in, deciphered, and added to server-side.
The new file would then be saved as a .js and a reference to it sent back from the server. The file would then reconstruct itself perfectly once read back in as JS. The beauty being that no hacky iterating or parsing is required for reconstruction.

How to use MutationObserver with an Object?

I have the following object:
mind = {
queries: [],
actions: []
};
and I update queries and actions according to another function.
I wanted to detect every time they're being updated and changed, and i've heard about MutationObserver, so I tried to call it:
var muob = (window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver);
var ob = new muob(function(m) {
console.log('It works!');
});
ob.observe(mind, { subtree: true });
But it doesn't work. I get in return:
Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
What's wrong with my code?
MutationObserver is only something that works for DOM elements, not objects:
var ob = new MutationObserver(function(m) {
console.log('It works!');
});
ob.observe(mind, { childList: true });
mind.textContent = 'foo';
<div id="mind"></div>
For what you're doing, you can make the queries and actions properties have methods to update the arrays instead, eg:
const mind = {
_queries: [],
_actions: [],
queries: {
push(...args) {
console.log('Push detected');
mind._queries.push(...args);
},
get() {
return mind._queries;
}
},
actions: {
push(...args) {
console.log('Push detected');
mind._actions.push(...args);
},
get() {
return mind._actions;
}
}
};
mind.queries.push('foo');
console.log(mind.queries.get());
Or, using a Proxy:
const handler = {
set(obj, prop, newVal) {
console.log('Change detected');
return obj[prop] = newVal;
},
get(obj, prop) {
return obj[prop];
}
};
const mind = {
queries: new Proxy([], handler),
actions: new Proxy([], handler),
};
mind.queries.push('foo');
console.log(mind.queries);
(the above snippet logs Change detected twice because it has to update both the 0 property on the array and change the array's .length)
Still, this is pretty odd - it would be much more elegant if, at the location in the code where you change the array, you also call another function (the It works! part) to indicate an update has occurred.
MutationObserver is for DOM elements, not JavaScript objects.
There is no equivalent for JavaScript objects,¹ but you can use a combination of Proxy objects and accessor properties to get a notification of any change to those arrays. You'd use the Proxy to know when the arrays were modified, and by making x and y accessor properties, you could know when they were changed (and use that as the opportunity to wrap them in proxies).
Here's a rough sketch:
const mind = (() => {
function wrap(array) {
return new Proxy(array, {
set(target, propName, value, receiver) {
beforeChange(target, propName, value, receiver);
const result = Reflect.set(target, propName, value);
afterChange(target, propName, value, receiver);
return result;
}
// ...you may want other traps here...
});
}
function beforeChange(target, name, value, receiver) {
console.log("beforeChange", name, value);
}
function afterChange(target, name, value, receiver) {
console.log("afterChange", name, value);
}
let queries = wrap([]);
let actions = wrap([]);
return {
get queries() {
return queries;
},
set queries(value) {
beforeChange(queries, "*queries*", value);
queries = wrap(value);
afterChange(queries, "*queries*", value);
},
get actions() {
return queries;
},
set queries(value) {
beforeChange(queries, "*actions*", value);
queries = wrap(value);
afterChange(queries, "*actions*", value);
}
};
})();
mind.queries.push(1);
mind.actions.push("two");
console.log(mind.actions);
mind.actions[0] = "TWO";
console.log(mind.actions);
mind.queries = [];
mind.queries[10] = "ten";
console.log(mind.queries);
.as-console-wrapper {
max-height: 100% !important;
}
¹ For a brief time there was going to be Object.observe, but it was abandoned in favor of Proxy.

Convert javascript object camelCase keys to underscore_case

I want to be able to pass any javascript object containing camelCase keys through a method and return an object with underscore_case keys, mapped to the same values.
So, I have this:
var camelCased = {firstName: 'Jon', lastName: 'Smith'}
And I want a method to output this:
{first_name: 'Jon', last_name: 'Jon'}
What's the fastest way to write a method that takes any object with any number of key/value pairs and outputs the underscore_cased version of that object?
Here's your function to convert camelCase to underscored text (see the jsfiddle):
function camelToUnderscore(key) {
return key.replace( /([A-Z])/g, "_$1").toLowerCase();
}
console.log(camelToUnderscore('helloWorldWhatsUp'));
Then you can just loop (see the other jsfiddle):
var original = {
whatsUp: 'you',
myName: 'is Bob'
},
newObject = {};
function camelToUnderscore(key) {
return key.replace( /([A-Z])/g, "_$1" ).toLowerCase();
}
for(var camel in original) {
newObject[camelToUnderscore(camel)] = original[camel];
}
console.log(newObject);
If you have an object with children objects, you can use recursion and change all properties:
function camelCaseKeysToUnderscore(obj){
if (typeof(obj) != "object") return obj;
for(var oldName in obj){
// Camel to underscore
newName = oldName.replace(/([A-Z])/g, function($1){return "_"+$1.toLowerCase();});
// Only process if names are different
if (newName != oldName) {
// Check for the old property name to avoid a ReferenceError in strict mode.
if (obj.hasOwnProperty(oldName)) {
obj[newName] = obj[oldName];
delete obj[oldName];
}
}
// Recursion
if (typeof(obj[newName]) == "object") {
obj[newName] = camelCaseKeysToUnderscore(obj[newName]);
}
}
return obj;
}
So, with an object like this:
var obj = {
userId: 20,
userName: "John",
subItem: {
paramOne: "test",
paramTwo: false
}
}
newobj = camelCaseKeysToUnderscore(obj);
You'll get:
{
user_id: 20,
user_name: "John",
sub_item: {
param_one: "test",
param_two: false
}
}
es6 node solution below. to use, require this file, then pass object you want converted into the function and it will return the camelcased / snakecased copy of the object.
const snakecase = require('lodash.snakecase');
const traverseObj = (obj) => {
const traverseArr = (arr) => {
arr.forEach((v) => {
if (v) {
if (v.constructor === Object) {
traverseObj(v);
} else if (v.constructor === Array) {
traverseArr(v);
}
}
});
};
Object.keys(obj).forEach((k) => {
if (obj[k]) {
if (obj[k].constructor === Object) {
traverseObj(obj[k]);
} else if (obj[k].constructor === Array) {
traverseArr(obj[k]);
}
}
const sck = snakecase(k);
if (sck !== k) {
obj[sck] = obj[k];
delete obj[k];
}
});
};
module.exports = (o) => {
if (!o || o.constructor !== Object) return o;
const obj = Object.assign({}, o);
traverseObj(obj);
return obj;
};
Came across this exact problem when working between JS & python/ruby objects. I noticed the accepted solution is using for in which will throw eslint error messages at you ref: https://github.com/airbnb/javascript/issues/851 which alludes to rule 11.1 re: use of pure functions rather than side effects ref:https://github.com/airbnb/javascript#iterators--nope
To that end, figured i'd share the below which passed the said rules.
import { snakeCase } from 'lodash'; // or use the regex in the accepted answer
camelCase = obj => {
const camelCaseObj = {};
for (const key of Object.keys(obj)){
if (Object.prototype.hasOwnProperty.call(obj, key)) {
camelCaseObj[snakeCase(key)] = obj[key];
}
}
return camelCaseObj;
};
Marcos Dimitrio posted above with his conversion function, which works but is not a pure function as it changes the original object passed in, which may be an undesireable side effect. Below returns a new object that doesn't modify the original.
export function camelCaseKeysToSnake(obj){
if (typeof(obj) != "object") return obj;
let newObj = {...obj}
for(var oldName in newObj){
// Camel to underscore
let newName = oldName.replace(/([A-Z])/g, function($1){return "_"+$1.toLowerCase();});
// Only process if names are different
if (newName != oldName) {
// Check for the old property name to avoid a ReferenceError in strict mode.
if (newObj.hasOwnProperty(oldName)) {
newObj[newName] = newObj[oldName];
delete newObj[oldName];
}
}
// Recursion
if (typeof(newObj[newName]) == "object") {
newObj[newName] = camelCaseKeysToSnake(newObj[newName]);
}
}
return newObj;
}
this library does exactly that: case-converter
It converts snake_case to camelCase and vice versa
const caseConverter = require('case-converter')
const snakeCase = {
an_object: {
nested_string: 'nested content',
nested_array: [{ an_object: 'something' }]
},
an_array: [
{ zero_index: 0 },
{ one_index: 1 }
]
}
const camelCase = caseConverter.toCamelCase(snakeCase);
console.log(camelCase)
/*
{
anObject: {
nestedString: 'nested content',
nestedArray: [{ anObject: 'something' }]
},
anArray: [
{ zeroIndex: 0 },
{ oneIndex: 1 }
]
}
*/
following what's suggested above, case-converter library is deprectaed, use snakecase-keys instead -
https://github.com/bendrucker/snakecase-keys
supports also nested objects & exclusions.
Any of the above snakeCase functions can be used in a reduce function as well:
const snakeCase = [lodash / case-converter / homebrew]
const snakeCasedObject = Object.keys(obj).reduce((result, key) => ({
...result,
[snakeCase(key)]: obj[key],
}), {})
jsfiddle
//This function will rename one property to another in place
Object.prototype.renameProperty = function (oldName, newName) {
// Do nothing if the names are the same
if (oldName == newName) {
return this;
}
// Check for the old property name to avoid a ReferenceError in strict mode.
if (this.hasOwnProperty(oldName)) {
this[newName] = this[oldName];
delete this[oldName];
}
return this;
};
//rename this to something like camelCase to snakeCase
function doStuff(object) {
for (var property in object) {
if (object.hasOwnProperty(property)) {
var r = property.replace(/([A-Z])/, function(v) { return '_' + v.toLowerCase(); });
console.log(object);
object.renameProperty(property, r);
console.log(object);
}
}
}
//example object
var camelCased = {firstName: 'Jon', lastName: 'Smith'};
doStuff(camelCased);
Note: remember to remove any and all console.logs as they aren't needed for production code

How do you JSON.stringify an ES6 Map?

I'd like to start using ES6 Map instead of JS objects but I'm being held back because I can't figure out how to JSON.stringify() a Map. My keys are guaranteed to be strings and my values will always be listed. Do I really have to write a wrapper method to serialize?
Both JSON.stringify and JSON.parse support a second argument. replacer and reviver respectively. With replacer and reviver below it's possible to add support for native Map object, including deeply nested values
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
Usage:
const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
Deep nesting with combination of Arrays, Objects and Maps
const originalValue = [
new Map([['a', {
b: {
c: new Map([['d', 'text']])
}
}]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
You can't directly stringify the Map instance as it doesn't have any properties, but you can convert it to an array of tuples:
jsonText = JSON.stringify(Array.from(map.entries()));
For the reverse, use
map = new Map(JSON.parse(jsonText));
You can't.
The keys of a map can be anything, including objects. But JSON syntax only allows strings as keys. So it's impossible in a general case.
My keys are guaranteed to be strings and my values will always be lists
In this case, you can use a plain object. It will have these advantages:
It will be able to be stringified to JSON.
It will work on older browsers.
It might be faster.
While there is no method provided by ecmascript yet, this can still be done using JSON.stingify if you map the Map to a JavaScript primitive. Here is the sample Map we'll use.
const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');
Going to an JavaScript Object
You can convert to JavaScript Object literal with the following helper function.
const mapToObj = m => {
return Array.from(m).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'
Going to a JavaScript Array of Objects
The helper function for this one would be even more compact
const mapToAoO = m => {
return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'
Going to Array of Arrays
This is even easier, you can just use
JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'
Using spread sytax Map can be serialized in one line:
JSON.stringify([...new Map()]);
and deserialize it with:
let map = new Map(JSON.parse(map));
Given your example is a simple use case in which keys are going to be simple types, I think this is the easiest way to JSON stringify a Map.
JSON.stringify(Object.fromEntries(map));
The way I think about the underlying data structure of a Map is as an array of key-value pairs (as arrays themselves). So, something like this:
const myMap = new Map([
["key1", "value1"],
["key2", "value2"],
["key3", "value3"]
]);
Because that underlying data structure is what we find in Object.entries, we can utilize the native JavaScript method of Object.fromEntries() on a Map as we would on an Array:
Object.fromEntries(myMap);
/*
{
key1: "value1",
key2: "value2",
key3: "value3"
}
*/
And then all you're left with is using JSON.stringify() on the result of that.
A Better Solution
// somewhere...
class Klass extends Map {
toJSON() {
var object = { };
for (let [key, value] of this) object[key] = value;
return object;
}
}
// somewhere else...
import { Klass as Map } from '#core/utilities/ds/map'; // <--wherever "somewhere" is
var map = new Map();
map.set('a', 1);
map.set('b', { datum: true });
map.set('c', [ 1,2,3 ]);
map.set( 'd', new Map([ ['e', true] ]) );
var json = JSON.stringify(map, null, '\t');
console.log('>', json);
Output
> {
"a": 1,
"b": {
"datum": true
},
"c": [
1,
2,
3
],
"d": {
"e": true
}
}
Hope that is less cringey than the answers above.
Stringify a Map instance (objects as keys are OK):
JSON.stringify([...map])
or
JSON.stringify(Array.from(map))
or
JSON.stringify(Array.from(map.entries()))
output format:
// [["key1","value1"],["key2","value2"]]
Below solution works even if you have nested Maps
function stringifyMap(myMap) {
function selfIterator(map) {
return Array.from(map).reduce((acc, [key, value]) => {
if (value instanceof Map) {
acc[key] = selfIterator(value);
} else {
acc[key] = value;
}
return acc;
}, {})
}
const res = selfIterator(myMap)
return JSON.stringify(res);
}
The very simple way.
const map = new Map();
map.set('Key1', "Value1");
map.set('Key2', "Value2");
console.log(Object.fromEntries(map));
`
Output:-
{"Key1": "Value1","Key2": "Value2"}
Just want to share my version for both Map and Set JSON.stringify only.
I'm sorting them, useful for debugging...
function replacer(key, value) {
if (value instanceof Map) {
const reducer = (obj, mapKey) => {
obj[mapKey] = value.get(mapKey);
return obj;
};
return [...value.keys()].sort().reduce(reducer, {});
} else if (value instanceof Set) {
return [...value].sort();
}
return value;
}
Usage:
const map = new Map();
const numbers= new Set()
numbers.add(3);
numbers.add(2);
numbers.add(3);
numbers.add(1);
const chars= new Set()
chars.add('b')
chars.add('a')
chars.add('a')
map.set("numbers",numbers)
map.set("chars",chars)
console.log(JSON.stringify(map, replacer, 2));
Result:
{
"chars": [
"a",
"b"
],
"numbers": [
1,
2,
3
]
}
You cannot call JSON.stringify on Map or Set.
You will need to convert:
the Map into a primitive Object, using Object.fromEntries, or
the Set into a primitive Array, using the spread operator [...]
…before calling JSON.stringify
Map
const
obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
map = new Map(Object.entries(obj));
map.set('Key3', 'Value3'); // Add a new entry
// Does NOT show the key-value pairs
console.log('Map:', JSON.stringify(map));
// Shows the key-value pairs
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Set
const
arr = ['Value1', 'Value2'],
set = new Set(arr);
set.add('Value3'); // Add a new item
// Does NOT show the values
console.log('Set:', JSON.stringify(set));
// Show the values
console.log(JSON.stringify([...set], null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
toJSON method
If you want to call JSON.stringify on a class object, you will need to override the toJSON method to return your instance data.
class Cat {
constructor(options = {}) {
this.name = options.name ?? '';
this.age = options.age ?? 0;
}
toString() {
return `[Cat name="${this.name}", age="${this.age}"]`
}
toJSON() {
return { name: this.name, age: this.age };
}
static fromObject(obj) {
const { name, age } = obj ?? {};
return new Cat({ name, age });
}
}
/*
* JSON Set adds the missing methods:
* - toJSON
* - toString
*/
class JSONSet extends Set {
constructor(values) {
super(values)
}
toString() {
return super
.toString()
.replace(']', ` ${[...this].map(v => v.toString())
.join(', ')}]`);
}
toJSON() {
return [...this];
}
}
const cats = new JSONSet([
Cat.fromObject({ name: 'Furball', age: 2 }),
Cat.fromObject({ name: 'Artemis', age: 5 })
]);
console.log(cats.toString());
console.log(JSON.stringify(cats, null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Correctly round-tripping serialization
Just copy this and use it. Or use the npm package.
const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);
// License: CC0
function stringifyReplacer(key, value) {
if (typeof value === "object" && value !== null) {
if (value instanceof Map) {
return {
_meta: { type: "map" },
value: Array.from(value.entries()),
};
} else if (value instanceof Set) { // bonus feature!
return {
_meta: { type: "set" },
value: Array.from(value.values()),
};
} else if ("_meta" in value) {
// Escape "_meta" properties
return {
...value,
_meta: {
type: "escaped-meta",
value: value["_meta"],
},
};
}
}
return value;
}
function parseReviver(key, value) {
if (typeof value === "object" && value !== null) {
if ("_meta" in value) {
if (value._meta.type === "map") {
return new Map(value.value);
} else if (value._meta.type === "set") {
return new Set(value.value);
} else if (value._meta.type === "escaped-meta") {
// Un-escape the "_meta" property
return {
...value,
_meta: value._meta.value,
};
} else {
console.warn("Unexpected meta", value._meta);
}
}
}
return value;
}
Why is this hard?
It should be possible to input any kind of data, get valid JSON, and from there correctly reconstruct the input.
This means dealing with
Maps that have objects as keys new Map([ [{cat:1}, "value"] ]). This means that any answer which uses Object.fromEntries is probably wrong.
Maps that have nested maps new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ]). A lot of answers sidestep this by only answering the question and not dealing with anything beyond that.
Mixing objects and maps {"key": new Map([ ["nested key", "nested value"] ]) }.
and on top of those difficulties, the serialisation format must be unambiguous. Otherwise one cannot always reconstruct the input. The top answer has one failing test case, see below.
Hence, I wrote this improved version. It uses _meta instead of dataType, to make conflicts rarer and if a conflict does happen, it actually unambiguously handles it. Hopefully the code is also simple enough to easily be extended to handle other containers.
My answer does, however, not attempt to handle exceedingly cursed cases, such as a map with object properties.
A test case for my answer, which demonstrates a few edge cases
const originalValue = [
new Map([['a', {
b: {
_meta: { __meta: "cat" },
c: new Map([['d', 'text']])
}
}]]),
{ _meta: { type: "map" }}
];
console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));
Accepted answer not round-tripping
The accepted answer is really lovely. However, it does not round trip when an object with a dataType property is passed it it.
// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue);
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different
I really don't know why there are so many long awesers here. This short version solved my problem:
const data = new Map()
data.set('visible', true)
data.set('child', new Map())
data.get('child').set('visible', false)
const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
// '{"visible":true,"child":{"visible":false}}'
const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
// Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }
The following method will convert a Map to a JSON string:
public static getJSONObj(): string {
return JSON.stringify(Object.fromEntries(map));
}
Example:
const x = new Map();
x.set("SomeBool", true);
x.set("number1", 1);
x.set("anObj", { name: "joe", age: 22, isAlive: true });
const json = getJSONObj(x);
// Output:
// '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'
Although there would be some scenarios where if you were the creator of the map you would write your code in a separate 'src' file and save a copy as a .txt file and, if written concisely enough, could easily be read in, deciphered, and added to server-side.
The new file would then be saved as a .js and a reference to it sent back from the server. The file would then reconstruct itself perfectly once read back in as JS. The beauty being that no hacky iterating or parsing is required for reconstruction.

Categories