JS update nested object key with string of key names concatenating with "." - javascript

Lets say that I have this object:
var obj = {
level1 :{
level2: {
level3: {
title: "winner"
}
}
}
}
Now I want to update the title key using the next string (notice, I have a string, not actual variable)
I have:
let myString = "level1.level2.level3.title"; // note - myString value comes from $http method or something
Maybe something like this:
obj[myString] = "super-winner";
Unfortunately the above doesn't work.
In addition - sometimes I need to update an undefined object so I need something to make the object to be defined with a new empty object.
For example, If I have the next object:
var obj = {
level1 : {}
}
}
I still want to modify the obj with the level3.winner as above.
Reminder:
obj[myString] = "super-winner";
How can I do that?

This works
const obj = {
// level1: {
// level2: {
// level3: {
// title: "winner"
// }
// }
// }
}
const myString = "level1.level2.level3.title"; // note - myString value comes from $http method or something
const title = 'super-winner'
myString.split('.')
.reduce(
(acc, curr) => {
if (acc[curr] === undefined && curr !== 'title') {
acc[curr] = {}
}
if (curr === 'title') {
acc[curr] = title
}
return acc[curr]
}, obj
);
console.log(obj) // {"level1":{"level2":{"level3":{"title":"super-winner"}}}}
This is zero-dependency solution, i.e. you don't have to use lodash or something bloating the size of your app.

Used "reduce" to achieve your desired result. Created a function "updateValue" where in you can pass obj - object to modify, str - property path to alter, value - value to be assigned at the property path
var obj1 = {
level1 :{
level2: {
level3: {
title: "winner"
}
}
}
}
var obj2 = { level1: {} }
var obj3 = {
level1 :{
level2: {
level3: {
title: "winner"
}
}
}
}
function updateValue(obj, str, value) {
let props = str.split('.'), arrIndex = -1
props.reduce((o,d,i) => (
arrIndex = d.indexOf('[') > -1 && d[d.indexOf('[') + 1],
arrIndex && (d = d.slice(0, d.indexOf('['))),
i == props.length - 1
? o[d] = value
: (o[d] = o[d] || {}, (arrIndex && (Array.isArray(o[d]) || (o[d] = [o[d]]))), arrIndex && o[d][arrIndex] || o[d])
)
, obj)
}
updateValue(obj1, 'level1.level2.level3.title', 'abcd')
updateValue(obj2, 'level1.level2.level3.title', 'abcd')
updateValue(obj3, 'level1.level2[0].title', 'abcd')
console.log(obj1)
console.log(obj2)
console.log(obj3)

This can be done by hand, indexing into the object structure repeatedly and creating new objects as necessary along the path to the destination key:
const updateField = (o, path, entry) => {
path = path.split(".");
let curr = o;
while (path.length > 1) {
const dir = path.shift();
const parent = curr;
curr = curr[dir];
if (undefined === curr) {
parent[dir] = {};
curr = parent[dir];
}
}
if (path.length === 1) {
curr[path.shift()] = entry;
}
return o;
};
var obj = {
level1 : {
level2: {
level3: {
title: "winner"
}
}
}
};
console.log(JSON.stringify(updateField(obj, "level1.level2.level3.title", "super-winner"), null, 2));
console.log(JSON.stringify(updateField({}, "level1.level2.level3.title", "super-winner"), null, 2));

You can use .set function of lodash https://lodash.com/docs#set
ex: _.set(obj, 'level1.level2.level3.title', 'super-winner');
Or use ES6 syntax function:
var str = 'level1.level2.level3.title';
str.split('.').reduce((p, c, index) => {
if (index === str.split('.').length - 1) {
if (typeof p[c] !== "object") { // string, number, boolean, null, undefined
p[c] = 'super-winner'
}
return p[c];
} else {
if (!p[c] || typeof p[c] !== 'object') {
p[c] = {};
}
return p[c];
}
}, obj)
console.log(obj);

Related

Replace all strings with empty string and all numbers with 0 in an object/array

I want to replace every string in and number in an object with an empty string of empty number.
For example:
const test = {
a: "asdf",
b: 2,
c: [
{
lala: "more strings",
d: "saaa"
}
]
}
with
const test = {
a: "",
b: 0,
c: [
{
lala: "",
d: ""
}
]
}
I can't think of an easy way to iterate over all the values in the object/array recursively. Is there a library (maybe lodash) that does this already?
A recursive implementation:
const test = { a: "asdf", b: 2, c: [{ lala: "more strings", d: "saaa", e: null }], g: 2.98 };
const isString = (val) => typeof val === 'string';
const isNumber = (val) => typeof val === 'number' && !Number.isNaN(val);
const isArray = (val) => Array.isArray(val);
const isObject = (val) => typeof val === 'object' && val !== null;
const modifyObject = (instance) => {
if (isString(instance)) return ""; //Base case
if (isNumber(instance)) return 0; //Base case
if (isArray(instance)) { //When instance is an Array we need to check further
return instance.map((item) => modifyObject(item));
}
if (isObject(instance)) { //When it's an object we need to recurse again to check if the value hits the base case or need to go further
return Object.entries(instance).reduce((acc, [k, v]) => ({
...acc,
[k]: modifyObject(v)
}), {});
}
return instance;
}
console.log(modifyObject(test));
Hope it helps!
The trickiest part would just be making sure you handle each type you could come across as you traverse an object/array.
Something like this should work:
function handleValue(val) {
if (typeof val === 'object')
if (val === null) {
return null
} else if (Array.isArray(val)) {
return handleArray(val)
} else {
return handleObject(val)
}
} else if (typeof val == "string") {
return ""
} else if (typeof val == 'number') {
return 0
}
}
function handleArray(arr) {
for (var i = 0; i < arr.length; i++) {
arr[i] = handleValue(arr[i])
}
return arr
}
function handleObject(obj) {
for (const [key, value] of Object.entries(obj)) {
obj[key] = handleValue(value)
}
return obj
}
You could use handleValue or handleObject on your example test object
Simple recursive solution:
const test = {
a: "asdf",
b: 2,
c: [{
lala: "more strings",
d: "saaa"
}]
}
const copy = replace(test)
console.log(copy)
function replace(value) {
if (typeof value === 'string') {
return ''
}
if (typeof value === 'number') {
return 0
}
if (typeof value !== 'object' || value === null) {
return value
}
if (Array.isArray(value)) {
return value.map(val => replace(val))
}
const obj = {}
for (const key in value) {
const val = value[key]
obj[key] = replace(val)
}
return obj
}

Filtering Everything But Functions

I have this object
{
helloWorld: function () {
console.log("Test")
},
debug: false,
foo: {
test: "test",
bar: function () {
console.log(false)
}
}
}
However, programmatically I want it to look like this:
{
helloWorld: function() {
console.log("Test")
},
foo: {
bar: function() {
console.log(false)
}
}
}
Basically removing everything but the functions of an object.
You could do a recursive call. For every key-value pairs of the object, check the value:
if it is function, keep it
else do a recursive call on that value
Base condition for on recursive call
if it is not object, return null
if the object is empty, also return null
After map through the key-value pairs, filter the pairs with value not equal null.
Finally, transform the pairs back to object
function keepFunc(obj) {
if (!isObject(obj)) {
return null
}
if (Object.keys(obj).length === 0) {
return null
}
return Object.fromEntries(
Object.entries(obj)
.map(([key, value]) => [
key,
isFunction(value) ? value : keepFunc(value)
])
.filter(([key, value]) => value !== null)
)
}
Runnable example
const obj = {
helloWorld: function() {
console.log('Test')
},
debug: false,
moreDebug: {},
foo: {
test: 'test',
bar: function() {
console.log(false)
},
moreTest: {
weather: 'cool',
say: function () {
console.log('phew')
}
}
}
}
const isObject = obj => typeof obj === 'object' && obj !== null
const isFunction = func => typeof func === 'function'
function keepFunc(obj) {
if (!isObject(obj)) {
return null
}
if (Object.keys(obj).length === 0) {
return null
}
return Object.fromEntries(
Object.entries(obj)
.map(([key, value]) => [
key,
isFunction(value) ? value : keepFunc(value)
])
.filter(([key, value]) => value !== null)
)
}
console.log(keepFunc(obj))
References
Object.entries(): to transform object into key-value pairs
Object.fromEntries(): to transform key-value pairs into object
You can use recursive function call in javascript to achieve that. For each key in the object check if it is an object or function and if it is keep it:
var input = {
helloWorld: function() {
console.log("Test")
},
debug: false,
foo: {
test: "test",
bar: function() {
console.log(false)
}
}
};
function buildObjectsOnlyObject(obj) {
let retVal = {};
for (let key in obj) {
const val = obj[key];
if (typeof val === 'object') {
if (!val) { // undefined and null also have object type
continue;
}
if (val.__proto__ === Array.prototype) { // check if object is an array
retVal[key] = val;
} else {
retVal[key] = buildObjectsOnlyObject(obj[key]);
}
} else if (typeof val === 'function') {
retVal[key] = val;
}
}
return retVal;
}
console.log(buildObjectsOnlyObject(input));
Object.entries and Object.fromEntries help here, as does Array.prototype.some.
For objects, filter the object's entries and keep only entries whose values are functions or objects with descendants that are functions. This can be checked recursively.
Then let each value be either the function or the stripped nested object.
This strip function will return undefined if nothing is kept.
let example =
{
helloWorld: function () {
console.log("Test")
},
debug: false,
foo: {
test: "test",
bar: function () {
console.log(false)
}
}
};
let isobj = val => typeof val == 'object' && val !== null;
let isfn = val => typeof val == 'function';
let keep = val => isobj(val) ? Object.entries(val).some(keepEntry) : isfn(val)
let keepEntry = ([key, val]) => keep(val);
let stripEntry = ([key, val]) => [key, strip(val)];
let strip = val => keep(val) ? isobj(val) ?
Object.fromEntries(Object.entries(val).filter(keep).map(stripEntry)) :
isfn(val) ? val : undefined : undefined
console.log(strip(example));
(Surprisingly this took more code than I thought it would.)

how to get keys of nested object

If I have a flat object then this works:
let stateCopy={...this.state}
Object.entries(dictionary).map(([key,value])=>{
stateCopy.key = value.toString())
})
Is there a way to do this if dictionary contains a nested object. Suppose a dictionary looks like:
dictionary={left:{name:'WORK',
min:2,
sec:0,}
start:true}
I need some way of updating stateCopy, i.e
stateCopy.left.name='WORK'
stateCopy.left.min=2
stateCopy.left.sec=0
stateCopy.start=true
function flattenDictionary(dict) {
if (!dict) {
return {};
}
/** This will hold the flattened keys/values */
const keys = {};
// Perform the flatten
flattenH(dict);
return keys;
function flattenH(obj, prefix) {
Object.keys(obj).forEach((key) => {
const val = obj[key];
/** This is what we pass forward as a new prefix, or is the flattened key */
let passKey;
// Only expect to see this when the original dictionary is passed as `obj`
if (!prefix || prefix === '') {
passKey = key;
} else {
// "Ignore" keys that are empty strings
passKey = ((key === '') ? prefix : `${prefix}.${key}`);
}
if (typeof obj[key] !== 'object') {
keys[passKey] = val;
} else {
flattenH(val, passKey);
}
});
}
}
Seems like you can do this with a little recursive function:
let state = {
left:{
start: "mark",
anotherLevel: {
test: 'leveltest'
}
},
test: "will be replaced"
}
let dictionary={
test2: {
foo: 'bar'
},
left:{
name:'WORK',
min:2,
sec:0,
anotherLevel: {
test_add: 'leveltest_add'
}
},
start:true,
test: 'replaced with me'
}
let stateCopy={...state}
function merge(obj, dict){
Object.entries(dict).forEach(([k, v]) =>{
if (!obj[k] || typeof v !== 'object') obj[k] = v
else merge(obj[k], v)
})
}
merge(stateCopy, dictionary)
console.log(stateCopy)

One liner to flatten nested object

I need to flatten a nested object. Need a one liner. Not sure what the correct term for this process is.
I can use pure Javascript or libraries, I particularly like underscore.
I've got ...
{
a:2,
b: {
c:3
}
}
And I want ...
{
a:2,
c:3
}
I've tried ...
var obj = {"fred":2,"jill":4,"obby":{"john":5}};
var resultObj = _.pick(obj, "fred")
alert(JSON.stringify(resultObj));
Which works but I also need this to work ...
var obj = {"fred":2,"jill":4,"obby":{"john":5}};
var resultObj = _.pick(obj, "john")
alert(JSON.stringify(resultObj));
Here you go:
Object.assign({}, ...function _flatten(o) { return [].concat(...Object.keys(o).map(k => typeof o[k] === 'object' ? _flatten(o[k]) : ({[k]: o[k]})))}(yourObject))
Summary: recursively create an array of one-property objects, then combine them all with Object.assign.
This uses ES6 features including Object.assign or the spread operator, but it should be easy enough to rewrite not to require them.
For those who don't care about the one-line craziness and would prefer to be able to actually read it (depending on your definition of readability):
Object.assign(
{},
...function _flatten(o) {
return [].concat(...Object.keys(o)
.map(k =>
typeof o[k] === 'object' ?
_flatten(o[k]) :
({[k]: o[k]})
)
);
}(yourObject)
)
Simplified readable example, no dependencies
/**
* Flatten a multidimensional object
*
* For example:
* flattenObject{ a: 1, b: { c: 2 } }
* Returns:
* { a: 1, c: 2}
*/
export const flattenObject = (obj) => {
const flattened = {}
Object.keys(obj).forEach((key) => {
const value = obj[key]
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(flattened, flattenObject(value))
} else {
flattened[key] = value
}
})
return flattened
}
Features
No dependencies
Works with null values
Works with arrays
Working example https://jsfiddle.net/webbertakken/jn613d8p/26/
Here is a true, crazy one-liner that flats the nested object recursively:
const flatten = (obj, roots=[], sep='.') => Object.keys(obj).reduce((memo, prop) => Object.assign({}, memo, Object.prototype.toString.call(obj[prop]) === '[object Object]' ? flatten(obj[prop], roots.concat([prop]), sep) : {[roots.concat([prop]).join(sep)]: obj[prop]}), {})
Multiline version, explained:
// $roots keeps previous parent properties as they will be added as a prefix for each prop.
// $sep is just a preference if you want to seperate nested paths other than dot.
const flatten = (obj, roots = [], sep = '.') => Object
// find props of given object
.keys(obj)
// return an object by iterating props
.reduce((memo, prop) => Object.assign(
// create a new object
{},
// include previously returned object
memo,
Object.prototype.toString.call(obj[prop]) === '[object Object]'
// keep working if value is an object
? flatten(obj[prop], roots.concat([prop]), sep)
// include current prop and value and prefix prop with the roots
: {[roots.concat([prop]).join(sep)]: obj[prop]}
), {})
An example:
const obj = {a: 1, b: 'b', d: {dd: 'Y'}, e: {f: {g: 'g'}}}
const flat = flatten(obj)
{
'a': 1,
'b': 'b',
'd.dd': 'Y',
'e.f.g': 'g'
}
Happy one-liner day!
ES6 Native, Recursive:
One-liner
const crushObj = (obj) => Object.keys(obj).reduce((acc, cur) => typeof obj[cur] === 'object' ? { ...acc, ...crushObj(obj[cur]) } : { ...acc, [cur]: obj[cur] } , {})
Expanded
const crushObj = (obj = {}) => Object.keys(obj || {}).reduce((acc, cur) => {
if (typeof obj[cur] === 'object') {
acc = { ...acc, ...crushObj(obj[cur])}
} else { acc[cur] = obj[cur] }
return acc
}, {})
Usage
const obj = {
a:2,
b: {
c:3
}
}
const crushed = crushObj(obj)
console.log(crushed)
// { a: 2, c: 3 }
My ES6 version:
const flatten = (obj) => {
let res = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object') {
res = { ...res, ...flatten(value) };
} else {
res[key] = value;
}
}
return res;
}
It's not quite a one liner, but here's a solution that doesn't require anything from ES6. It uses underscore's extend method, which could be swapped out for jQuery's.
function flatten(obj) {
var flattenedObj = {};
Object.keys(obj).forEach(function(key){
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
});
return flattenedObj;
}
I like this code because it's a bit easier to understand.
Edit: I added some functionality I needed, so now it's a bit harder to understand.
const data = {
a: "a",
b: {
c: "c",
d: {
e: "e",
f: [
"g",
{
i: "i",
j: {},
k: []
}
]
}
}
};
function flatten(data, response = {}, flatKey = "", onlyLastKey = false) {
for (const [key, value] of Object.entries(data)) {
let newFlatKey;
if (!isNaN(parseInt(key)) && flatKey.includes("[]")) {
newFlatKey = (flatKey.charAt(flatKey.length - 1) == "." ? flatKey.slice(0, -1) : flatKey) + `[${key}]`;
} else if (!flatKey.includes(".") && flatKey.length > 0) {
newFlatKey = `${flatKey}.${key}`;
} else {
newFlatKey = `${flatKey}${key}`;
}
if (typeof value === "object" && value !== null && Object.keys(value).length > 0) {
flatten(value, response, `${newFlatKey}.`, onlyLastKey);
} else {
if(onlyLastKey){
newFlatKey = newFlatKey.split(".").pop();
}
if (Array.isArray(response)) {
response.push({
[newFlatKey.replace("[]", "")]: value
});
} else {
response[newFlatKey.replace("[]", "")] = value;
}
}
}
return response;
}
console.log(flatten(data));
console.log(flatten(data, {}, "data"));
console.log(flatten(data, {}, "data[]"));
console.log(flatten(data, {}, "data", true));
console.log(flatten(data, {}, "data[]", true));
console.log(flatten(data, []));
console.log(flatten(data, [], "data"));
console.log(flatten(data, [], "data[]"));
console.log(flatten(data, [], "data", true));
console.log(flatten(data, [], "data[]", true));
Demo https://stackblitz.com/edit/typescript-flatter
For insinde a typescript class use:
function flatten(data: any, response = {}, flatKey = "", onlyLastKey = false) {
for (const [key, value] of Object.entries(data)) {
let newFlatKey: string;
if (!isNaN(parseInt(key)) && flatKey.includes("[]")) {
newFlatKey = (flatKey.charAt(flatKey.length - 1) == "." ? flatKey.slice(0, -1) : flatKey) + `[${key}]`;
} else if (!flatKey.includes(".") && flatKey.length > 0) {
newFlatKey = `${flatKey}.${key}`;
} else {
newFlatKey = `${flatKey}${key}`;
}
if (typeof value === "object" && value !== null && Object.keys(value).length > 0) {
flatten(value, response, `${newFlatKey}.`, onlyLastKey);
} else {
if(onlyLastKey){
newFlatKey = newFlatKey.split(".").pop();
}
if (Array.isArray(response)) {
response.push({
[newFlatKey.replace("[]", "")]: value
});
} else {
response[newFlatKey.replace("[]", "")] = value;
}
}
}
return response;
}
Here's an ES6 version in TypeScript. It takes the best of answers given here and elsewhere. Some features:
Supports Date objects and converts them into ISO strings
Puts an underscore between the parent's and child's key (e.g. {a: {b: 'test'}} becomes {a_b: 'test'}
const flatten = (obj: Record<string, unknown>, parent?: string): Record<string, unknown> => {
let res: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
const propName = parent ? parent + '_' + key : key
const flattened: Record<string, unknown> = {}
if (value instanceof Date) {
flattened[key] = value.toISOString()
} else if(typeof value === 'object' && value !== null){
res = {...res, ...flatten(value as Record<string, unknown>, propName)}
} else {
res[propName] = value
}
}
return res
}
An example:
const example = {
person: {
firstName: 'Demo',
lastName: 'Person'
},
date: new Date(),
hello: 'world'
}
// becomes
const flattenedExample = {
person_firstName: 'Demo',
person_lastName: 'Person',
date: '2021-10-18T10:41:14.278Z',
hello: 'world'
}
This is a function I've got in my common libraries for exactly this purpose.
I believe I got this from a similar stackoverflow question, but cannot remember which (edit: Fastest way to flatten / un-flatten nested JSON objects - Thanks Yoshi!)
function flatten(data) {
var result = {};
function recurse (cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
for(var i=0, l=cur.length; i<l; i++)
recurse(cur[i], prop + "[" + i + "]");
if (l == 0)
result[prop] = [];
} else {
var isEmpty = true;
for (var p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop+"."+p : p);
}
if (isEmpty && prop)
result[prop] = {};
}
}
recurse(data, "");
return result;
}
This can then be called as follows:
var myJSON = '{a:2, b:{c:3}}';
var myFlattenedJSON = flatten(myJSON);
You can also append this function to the standard Javascript string class as follows:
String.prototype.flattenJSON = function() {
var data = this;
var result = {};
function recurse (cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
for(var i=0, l=cur.length; i<l; i++)
recurse(cur[i], prop + "[" + i + "]");
if (l == 0)
result[prop] = [];
} else {
var isEmpty = true;
for (var p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop+"."+p : p);
}
if (isEmpty && prop)
result[prop] = {};
}
}
recurse(data, "");
return result;
}
With which, you can do the following:
var flattenedJSON = '{a:2, b:{c:3}}'.flattenJSON();
Here are vanilla solutions that work for arrays, primitives, regular expressions, functions, any number of nested object levels, and just about everything else I could throw at them. The first overwrites property values in the manner that you would expect from Object.assign.
((o) => {
return o !== Object(o) || Array.isArray(o) ? {}
: Object.assign({}, ...function leaves(o) {
return [].concat.apply([], Object.entries(o)
.map(([k, v]) => {
return (( !v || typeof v !== 'object'
|| !Object.keys(v).some(key => v.hasOwnProperty(key))
|| Array.isArray(v))
? {[k]: v}
: leaves(v)
);
})
);
}(o))
})(o)
The second accumulates values into an array.
((o) => {
return o !== Object(o) || Array.isArray(o) ? {}
: (function () {
return Object.values((function leaves(o) {
return [].concat.apply([], !o ? [] : Object.entries(o)
.map(([k, v]) => {
return (( !v || typeof v !== 'object'
|| !Object.keys(v).some(k => v.hasOwnProperty(k))
|| (Array.isArray(v) && !v.some(el => typeof el === 'object')))
? {[k]: v}
: leaves(v)
);
})
);
}(o))).reduce((acc, cur) => {
return ((key) => {
acc[key] = !acc[key] ? [cur[key]]
: new Array(...new Set(acc[key].concat([cur[key]])))
})(Object.keys(cur)[0]) ? acc : acc
}, {})
})(o);
})(o)
Also please do not include code like this in production as it is terribly difficult to debug.
function leaves1(o) {
return ((o) => {
return o !== Object(o) || Array.isArray(o) ? {}
: Object.assign({}, ...function leaves(o) {
return [].concat.apply([], Object.entries(o)
.map(([k, v]) => {
return (( !v || typeof v !== 'object'
|| !Object.keys(v).some(key => v.hasOwnProperty(key))
|| Array.isArray(v))
? {[k]: v}
: leaves(v)
);
})
);
}(o))
})(o);
}
function leaves2(o) {
return ((o) => {
return o !== Object(o) || Array.isArray(o) ? {}
: (function () {
return Object.values((function leaves(o) {
return [].concat.apply([], !o ? [] : Object.entries(o)
.map(([k, v]) => {
return (( !v || typeof v !== 'object'
|| !Object.keys(v).some(k => v.hasOwnProperty(k))
|| (Array.isArray(v) && !v.some(el => typeof el === 'object')))
? {[k]: v}
: leaves(v)
);
})
);
}(o))).reduce((acc, cur) => {
return ((key) => {
acc[key] = !acc[key] ? [cur[key]]
: new Array(...new Set(acc[key].concat([cur[key]])))
})(Object.keys(cur)[0]) ? acc : acc
}, {})
})(o);
})(o);
}
const obj = {
l1k0: 'foo',
l1k1: {
l2k0: 'bar',
l2k1: {
l3k0: {},
l3k1: null
},
l2k2: undefined
},
l1k2: 0,
l2k3: {
l3k2: true,
l3k3: {
l4k0: [1,2,3],
l4k1: [4,5,'six', {7: 'eight'}],
l4k2: {
null: 'test',
[{}]: 'obj',
[Array.prototype.map]: Array.prototype.map,
l5k3: ((o) => (typeof o === 'object'))(this.obj),
}
}
},
l1k4: '',
l1k5: new RegExp(/[\s\t]+/g),
l1k6: function(o) { return o.reduce((a,b) => a+b)},
false: [],
}
const objs = [null, undefined, {}, [], ['non', 'empty'], 42, /[\s\t]+/g, obj];
objs.forEach(o => {
console.log(leaves1(o));
});
objs.forEach(o => {
console.log(leaves2(o));
});
Here is an actual oneliner of just 91 characters, using Underscore. (Of course. What else?)
var { reduce, isObject } = _;
var data = {
a: 1,
b: 2,
c: {
d: 3,
e: 4,
f: {
g: 5
},
h: 6
}
};
var tip = (v, m={}) => reduce(v, (m, v, k) => isObject(v) ? tip(v, m) : {...m, [k]: v}, m);
console.log(tip(data));
<script src="https://underscorejs.org/underscore-umd-min.js"></script>
Readable version:
var { reduce, isObject, extend } = _;
var data = {
a: 1,
b: 2,
c: {
d: 3,
e: 4,
f: {
g: 5
},
h: 6
}
};
// This function is passed to _.reduce below.
// We visit a single key of the input object. If the value
// itself is an object, we recursively copy its keys into
// the output object (memo) by calling tip. Otherwise we
// add the key-value pair to the output object directly.
function tipIteratee(memo, value, key) {
if (isObject(value)) return tip(value, memo);
return extend(memo, {[key]: value});
}
// The entry point of the algorithm. Walks over the keys of
// an object using _.reduce, collecting all tip keys in memo.
function tip(value, memo = {}) {
return _.reduce(value, tipIteratee, memo);
}
console.log(tip(data));
<script src="https://underscorejs.org/underscore-umd-min.js"></script>
Also works with Lodash.
To flatten only the first level of the object and merge duplicate object keys into an array:
var myObj = {
id: '123',
props: {
Name: 'Apple',
Type: 'Fruit',
Link: 'apple.com',
id: '345'
},
moreprops: {
id: "466"
}
};
const flattenObject = (obj) => {
let flat = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
for (const [subkey, subvalue] of Object.entries(value)) {
// avoid overwriting duplicate keys: merge instead into array
typeof flat[subkey] === 'undefined' ?
flat[subkey] = subvalue :
Array.isArray(flat[subkey]) ?
flat[subkey].push(subvalue) :
flat[subkey] = [flat[subkey], subvalue]
}
} else {
flat = {...flat, ...{[key]: value}};
}
}
return flat;
}
console.log(flattenObject(myObj))
Object.assign requires a polyfill. This version is similar to previous ones, but it is not using Object.assign and it is still keep tracking of parent's name
const flatten = (obj, parent = null) => Object.keys(obj).reduce((acc, cur) =>
typeof obj[cur] === 'object' ? { ...acc, ...flatten(obj[cur], cur) } :
{ ...acc, [((parent) ? parent + '.' : "") + cur]: obj[cur] } , {})
const obj = {
a:2,
b: {
c:3
}
}
const flattened = flatten(obj)
console.log(flattened)
Here is a flatten function that correctly outputs array indexes.
function flatten(obj) {
const result = {};
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'object') {
const nested = flatten(obj[key]);
for (const nestedKey of Object.keys(nested)) {
result[`${key}.${nestedKey}`] = nested[nestedKey];
}
} else {
result[key] = obj[key];
}
}
return result;
}
Example Input:
{
"first_name": "validations.required",
"no_middle_name": "validations.required",
"last_name": "validations.required",
"dob": "validations.required",
"citizenship": "validations.required",
"citizenship_identity": {
"name": "validations.required",
"value": "validations.required"
},
"address": [
{
"country_code": "validations.required",
"street": "validations.required",
"city": "validations.required",
"state": "validations.required",
"zipcode": "validations.required",
"start_date": "validations.required",
"end_date": "validations.required"
},
{
"country_code": "validations.required",
"street": "validations.required",
"city": "validations.required",
"state": "validations.required",
"zipcode": "validations.required",
"start_date": "validations.required",
"end_date": "validations.required"
}
]
}
Example Output:
const flattenedOutput = flatten(inputObj);
{
"first_name": "validations.required",
"no_middle_name": "validations.required",
"last_name": "validations.required",
"dob": "validations.required",
"citizenship": "validations.required",
"citizenship_identity.name": "validations.required",
"citizenship_identity.value": "validations.required",
"address.0.country_code": "validations.required",
"address.0.street": "validations.required",
"address.0.city": "validations.required",
"address.0.state": "validations.required",
"address.0.zipcode": "validations.required",
"address.0.start_date": "validations.required",
"address.0.end_date": "validations.required",
"address.1.country_code": "validations.required",
"address.1.street": "validations.required",
"address.1.city": "validations.required",
"address.1.state": "validations.required",
"address.1.zipcode": "validations.required",
"address.1.start_date": "validations.required",
"address.1.end_date": "validations.required"
}
function flatten(obj: any) {
return Object.keys(obj).reduce((acc, current) => {
const key = `${current}`;
const currentValue = obj[current];
if (Array.isArray(currentValue) || Object(currentValue) === currentValue) {
Object.assign(acc, flatten(currentValue));
} else {
acc[key] = currentValue;
}
return acc;
}, {});
};
let obj = {
a:2,
b: {
c:3
}
}
console.log(flatten(obj))
Demo
https://stackblitz.com/edit/typescript-flatten-json
Here goes, not thoroughly tested. Utilizes ES6 syntax too!!
loopValues(val){
let vals = Object.values(val);
let q = [];
vals.forEach(elm => {
if(elm === null || elm === undefined) { return; }
if (typeof elm === 'object') {
q = [...q, ...this.loopValues(elm)];
}
return q.push(elm);
});
return q;
}
let flatValues = this.loopValues(object)
flatValues = flatValues.filter(elm => typeof elm !== 'object');
console.log(flatValues);
I know its been very long, but it may be helpful for some one in the future
I've used recursion
let resObj = {};
function flattenObj(obj) {
for (let key in obj) {
if (!(typeof obj[key] == 'object')) {
// console.log('not an object', key);
resObj[key] = obj[key];
// console.log('res obj is ', resObj);
} else {
flattenObj(obj[key]);
}
}
return resObj;
}
Here's my TypeScript extension from #Webber's answer. Also supports dates:
private flattenObject(obj: any): any {
const flattened = {};
for (const key of Object.keys(obj)) {
if (isNullOrUndefined(obj[key])) {
continue;
}
if (typeof obj[key].getMonth === 'function') {
flattened[key] = (obj[key] as Date).toISOString();
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
Object.assign(flattened, this.flattenObject(obj[key]));
} else {
flattened[key] = obj[key];
}
}
return flattened;
}
const obj = {
a:2,
b: {
c:3
}
}
// recursive function for extracting keys
function extractKeys(obj) {
let flattenedObj = {};
for(let [key, value] of Object.entries(obj)){
if(typeof value === "object") {
flattenedObj = {...flattenedObj, ...extractKeys(value)};
} else {
flattenedObj[key] = value;
}
}
return flattenedObj;
}
// main code
let flattenedObj = extractKeys(obj);
console.log(flattenedObj);

How to get the path from javascript object from key and value

I have a javascript object width depth.
I need to know the exact path from this key within the object ex: "obj1.obj2.data1"
I already know the key is data1, the value is 123.
My javascript object look like this
{
obj1: {
obj2: {
data1: 213,
data2: "1231",
obj3: {
data: "milf"
}
}
},
obj4: {
description: "toto"
}
}
How could I achieve that ?
here is a jsfiddle : http://jsfiddle.net/3hvav8xf/8/
I am trying to implement getPath.
I think recursive function can help to you (Updated version, to check value)
function path(c, name, v, currentPath, t){
var currentPath = currentPath || "root";
for(var i in c){
if(i == name && c[i] == v){
t = currentPath;
}
else if(typeof c[i] == "object"){
return path(c[i], name, v, currentPath + "." + i);
}
}
return t + "." + name;
};
console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "s", 1));
The following finds the path in any level of nested objects. Also with arrays.
It returns all the paths found, which is something you want if you have keys with the same name.
I like this approach because it works with lodash methods get and set out-of-the-box.
function findPathsToKey(options) {
let results = [];
(function findKey({
key,
obj,
pathToKey,
}) {
const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
if (obj.hasOwnProperty(key)) {
results.push(`${oldPath}${key}`);
return;
}
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
for (const k in obj) {
if (obj.hasOwnProperty(k)) {
if (Array.isArray(obj[k])) {
for (let j = 0; j < obj[k].length; j++) {
findKey({
obj: obj[k][j],
key,
pathToKey: `${oldPath}${k}[${j}]`,
});
}
}
if (obj[k] !== null && typeof obj[k] === "object") {
findKey({
obj: obj[k],
key,
pathToKey: `${oldPath}${k}`,
});
}
}
}
}
})(options);
return results;
}
findPathsToKey({ obj: objWithDuplicates, key: "d" })
// ["parentKey.arr[0].c.d", "parentKey.arr[1].c.d", "parentKey.arr[2].c.d"]
Try it here - https://jsfiddle.net/spuhb8v7/1/
If you want the result to be a single key (first encountered), you can change the results to be a string and if defined, then return the function with it.
I ended up with the following function, that works with nested objects/arrays :
function findPath (obj, name, val, currentPath) {
currentPath = currentPath || ''
let matchingPath
if (!obj || typeof obj !== 'object') return
if (obj[name] === val) return `${currentPath}['${name}']`
for (const key of Object.keys(obj)) {
if (key === name && obj[key] === val) {
matchingPath = currentPath
} else {
matchingPath = findPath(obj[key], name, val, `${currentPath}['${key}']`)
}
if (matchingPath) break
}
return matchingPath
}
const treeData = [{
id: 1,
children: [{
id: 2
}]
}, {
id: 3,
children: [{
id: 4,
children: [{
id: 5
}]
}]
}]
console.log(findPath (treeData, 'id', 5))
Here you go!
function getPath(obj, value, path) {
if(typeof obj !== 'object') {
return;
}
for(var key in obj) {
if(obj.hasOwnProperty(key)) {
console.log(key);
var t = path;
var v = obj[key];
if(!path) {
path = key;
}
else {
path = path + '.' + key;
}
if(v === value) {
return path;
}
else if(typeof v !== 'object'){
path = t;
}
var res = getPath(v, value, path);
if(res) {
return res;
}
}
}
}
getPath(yourObject, valueYouWantToFindPath);
Rerutns path if found, else returns undefined.
I have only tested it with objects & comparison is very strict(ie: used ===).
Update:
Updated version that takes key as an argument.
function getPath(obj, key, value, path) {
if(typeof obj !== 'object') {
return;
}
for(var k in obj) {
if(obj.hasOwnProperty(k)) {
console.log(k);
var t = path;
var v = obj[k];
if(!path) {
path = k;
}
else {
path = path + '.' + k;
}
if(v === value) {
if(key === k) {
return path;
}
else {
path = t;
}
}
else if(typeof v !== 'object'){
path = t;
}
var res = getPath(v, key, value, path);
if(res) {
return res;
}
}
}
}
getPath(yourObject, key, valueYouWantToFindPath);
JSON Object can be handled in JavaScript as associative array.
So You can cycle through and store indexes of "parents" in some variables.
Assume the whole object to be stored in variable called obj.
for( var p1 in obj )
{
for( var p2 in obj[ p1 ] )
{
for( var p3 in obj[ p1 ][ p2 ] )
{
// obj[ p1 ][ p2 ][ p3 ] is current node
// so for Your example it is obj.obj1.obj2.data1
}
}
}
Hope answer was helpful.
I would do this job as follows;
Object.prototype.paths = function(root = [], result = {}) {
var ok = Object.keys(this);
return ok.reduce((res,key) => { var path = root.concat(key);
typeof this[key] === "object" &&
this[key] !== null ? this[key].paths(path,res)
: res[this[key]] == 0 || res[this[key]] ? res[this[key]].push(path)
: res[this[key]] = [path];
return res;
},result);
};
var myObj = {
obj1: {
obj2: {
data1: 213,
data2: "1231",
obj3: {
data: "milf"
}
}
},
obj4: {
description: "toto",
cougars: "Jodi",
category: "milf"
}
},
value = "milf",
milfPath = myObj.paths()[value]; // the value can be set dynamically and if exists it's path will be listed.
console.log(milfPath);
A few words of warning: We should be cautious when playing with the Object prototype. Our modification should have the descriptor enumerable = false or it will list in the for in loops and for instance jQuery will not work. (this is how silly jQuery is, since apparently they are not making a hasOwnProperty check in their for in loops) Some good reads are here and here So we have to add this Object method with Object.defineProperty() to make it enumerable = false;. But for the sake of simplicity and to stay in the scope of the question i haven't included that part in the code.
Here is a pretty short, and relatively easy to understand function I wrote for retrieving the JSON Path for every property/field on an Object (no matter how deeply nested, or not).
The getPaths(object) function just takes the Object you'd like the JSON Paths for and returns an array of paths. OR, if you would like the initial object to be denoted with a symbol that is different from the standard JSON Path symbol, $, you can call getPaths(object, path), and each JSON Path will begin with the specified path.
For Example: getPaths({prop: "string"}, 'obj'); would return the following JSON Path: obj.prop, rather than $.prop.
See below for a more detailed, in depth example of what getPaths returns, and how it is used.
object = {
"firstName": "John",
"lastName": "doe",
"age": 26,
"fakeData": true,
"address": {
"streetAddress": "fake street",
"city": "fake city",
"postalCode": "12345"
},
"phoneNumbers": [{
"type": "iPhone",
"number": "0123-4567-8888"
}, {
"type": "home",
"number": "0123-4567-8910"
}]
};
function getPaths(object, path = "$") {
return Object.entries(object).flatMap(function(o, i) {
if (typeof o[1] === "object" && !o[1].length) {
return `${getPaths(o[1], path + '.' + o[0])}`.split(',');
} else if (typeof o[1] === "object" && o[1].length) {
return Object.entries(o[1]).flatMap((no, i) => getPaths(no[1], `${path}.${o[0]}[${i}]`));
} else {
return `${path}.${o[0]}`;
}
});
}
console.log(`%o`, getPaths(object));
I really liked Roland Jegorov's answer, but I had a very complex object that I needed to search through and that answer could not account for it.
If you were in a situation like mine you may want to first make sure you have no circular references (or else you'll run into an infinite search). There are a few ways to do this, but I was having to stringify my object to copy it into other windows, so I ended up using this circular replacer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
(Update here - I made a small change to the getCircularReplacer function from MDN so it no longer leaves out function references since that is what I was looking for!)
(Update 3 - I also wanted to check on methods of any instances of classes, but I was returning just 'function' too early, so I have adjusted it to include instance methods. I think it finally works as I intended!)
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "function") {
if (value?.prototype) {
if (seen.has(value.prototype)) {
return;
}
seen.add(value.prototype)
return value.prototype
}
return "function";
}
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
const nonCyclicObject = JSON.parse(JSON.stringify(myComplexObject, getCircularReplacer()));
Then I used this modified version of Roland's answer:
(Update 2: I had to make sure not to return after the key was found as it would always simply return after only calling the function once if the first level of the object had that key)
function findPathsToKey(options) {
let count = 0;
let results = [];
(function findKey({
key,
obj,
pathToKey,
}) {
count += 1;
if (obj === null) return;
const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
if (Object.hasOwnProperty.call(obj, key)) {
results.push(`${oldPath}${key}`);
}
if (typeof obj === "object" && !Array.isArray(obj)) {
for (const k in obj) {
if (Object.hasOwnProperty.call(obj, k)) {
if (Array.isArray(obj[k])) {
for (let j = 0; j < obj[k].length; j++) {
findKey({
obj: obj[k][j],
key,
pathToKey: `${oldPath}${k}[${j}]`,
});
}
}
if (typeof obj[k] === "object") {
findKey({
obj: obj[k],
key,
pathToKey: `${oldPath}${k}`,
});
}
}
}
}
})(options);
return { count, results };
};
The count was just to troubleshoot a little bit and make sure it was actually running through the amount of keys I thought it was. Hope this helps any others looking for a solution!
⚠️ This code doesn't answer the question but does related: transforms nested object to query object with dot.divided.path as keys and non-object values; compatible with URlSearchParams & qs. Maybe will be useful for someone.
const isPlainObject = (v) => {
if (Object.prototype.toString.call(v) !== '[object Object]') return false;
const prototype = Object.getPrototypeOf(v);
return prototype === null || prototype === Object.prototype;
};
const objectToQueryObject = (obj, path) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
const newPath = path ? `${path}.${key}` : key;
if (isPlainObject(value)) {
return {
...acc,
...objectToQueryObject(value, newPath)
};
}
acc[newPath] = value;
return acc;
}, {})
};
const queryObjectRaw = {
value: {
field: {
array: {
'[*]': {
field2: {
eq: 'foo',
ne: 'bar',
}
}
},
someOtherProp: { in: [1, 2, 3],
ne: 'baz',
}
},
someOtherField: {
gt: 123
},
},
otherValue: {
eq: 2
},
};
const result = objectToQueryObject(queryObjectRaw);
console.log('result', result);
const queryString = new URLSearchParams(result).toString();
console.log('queryString', queryString);
If you know only the value and not the key, and want to find all paths with this value use this.
It will find all property with that value, and print the complete path for every founded value.
const createArrayOfKeys = (obj, value) => {
const result = []
function iter(o) {
Object.keys(o).forEach(function(k) {
if (o[k] !== null && typeof o[k] === 'object') {
iter(o[k])
return
}
if (o[k]=== value) {
result.push(k)
return
}
})
}
iter(obj)
return result
}
function findPath (obj, name, val, currentPath) {
currentPath = currentPath || ''
let matchingPath
if (!obj || typeof obj !== 'object') return
if (obj[name] === val) return `${currentPath}/${name}/${val}`
for (const key of Object.keys(obj)) {
if (key === name && obj[key] === val) {
matchingPath = currentPath
} else {
matchingPath = findPath(obj[key], name, val, `${currentPath}/${key}`)
}
if (matchingPath) break
}
return matchingPath
}
const searchMultiplePaths = (obj, value) => {
const keys = createArrayOfKeys(obj, value)
console.log(keys);
keys.forEach(key => {
console.log(findPath(obj, key, value))
})
}
var data = { ffs: false, customer: { customer_id: 1544248, z_cx_id: '123456' }, selected_items: { '3600196': [{ id: 4122652, name: 'Essential Large (up to 8\'x10\')', selected: true }] }, service_partner: { id: 3486, name: 'Some String', street: '1234 King St.', hop: '123456' }, subject: 'Project-2810191 - Orange Juice Stain (Rug)', description: 'Product Type: \n\nIssue: (copy/paste service request details here)\n\nAction Required:', yes: '123456' };
searchMultiplePaths(data, '123456')
I know the post is old but the answers don't really satisfy me.
A simple solution is to add the object path to each object in the structure. Then you can easily read the path when you need it.
let myObject = {
name: 'abc',
arrayWithObject: [
{
name: "def"
},
{
name: "ghi",
obj: {
name: "jkl"
}
}
],
array: [15, 'mno'],
arrayArrayObject: [
[
{
name: '...'
}
]
]
}
function addPath(obj, path = [], objectPathKey = '_path') {
if (Array.isArray(obj)) {
obj.map((item, idx) => addPath(item, [...path, idx]))
} else if (typeof obj === "object") {
obj[objectPathKey] = path;
for (const key in obj) {
obj[key] = addPath(obj[key], [...path, key])
}
}
return obj
}
myObject = addPath(myObject);
let changeMe = _.cloneDeep(myObject.arrayWithObject[0])
changeMe.newProp = "NEW"
changeMe.newNested = {name: "new", deeper: {name: "asdasda"}}
changeMe = addPath(changeMe, changeMe._path)
_.set(myObject, changeMe._path, changeMe);
When your updates are done sanitize your object and remove your _path property.
Advantages of this solution:
You do the work once
you keep your code simple
no need for own property checks
no cognitive overload
I can highly suggest you to use lodash for this problem.
In their documentation this should help you out
// using "_.where" callback shorthand
_.find(characters, { 'age': 1 });
// → { 'name': 'pebbles', 'age': 1, 'blocked': false }

Categories

Resources