Flatten a javascript object to pass as querystring - javascript

I have a javascript object that I need to flatten into a string so that I can pass as querystring, how would I do that? i.e:
{ cost: 12345, insertBy: 'testUser' } would become cost=12345&insertBy=testUser
I can't use jQuery AJAX call for this call, I know we can use that and pass the object in as data but not in this case. Using jQuery to flatten to object would be okay though.
Thank you.

Here's a non-jQuery version:
function toQueryString(obj) {
var parts = [];
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i]));
}
}
return parts.join("&");
}

You want jQuery.param:
var str = $.param({ cost: 12345, insertBy: 'testUser' });
// "cost=12345&insertBy=testUser"
Note that this is the function used internally by jQuery to serialize objects passed as the data argument.

My ES6 version (pure Javascript, no jQuery):
function toQueryString(paramsObject) {
return Object
.keys(paramsObject)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(paramsObject[key])}`)
.join('&')
;
}

This is an old question, but at the top of Google searches, so I'm adding this for completeness.
If 1) you don't want to user jQuery, but 2) you want to covert a nested object to a query string, then (building off of Tim Down and Guy's answers), use this:
function toQueryString(obj, urlEncode) {
//
// Helper function that flattens an object, retaining key structer as a path array:
//
// Input: { prop1: 'x', prop2: { y: 1, z: 2 } }
// Example output: [
// { path: [ 'prop1' ], val: 'x' },
// { path: [ 'prop2', 'y' ], val: '1' },
// { path: [ 'prop2', 'z' ], val: '2' }
// ]
//
function flattenObj(x, path) {
var result = [];
path = path || [];
Object.keys(x).forEach(function (key) {
if (!x.hasOwnProperty(key)) return;
var newPath = path.slice();
newPath.push(key);
var vals = [];
if (typeof x[key] == 'object') {
vals = flattenObj(x[key], newPath);
} else {
vals.push({ path: newPath, val: x[key] });
}
vals.forEach(function (obj) {
return result.push(obj);
});
});
return result;
} // flattenObj
// start with flattening `obj`
var parts = flattenObj(obj); // [ { path: [ ...parts ], val: ... }, ... ]
// convert to array notation:
parts = parts.map(function (varInfo) {
if (varInfo.path.length == 1) varInfo.path = varInfo.path[0];else {
var first = varInfo.path[0];
var rest = varInfo.path.slice(1);
varInfo.path = first + '[' + rest.join('][') + ']';
}
return varInfo;
}); // parts.map
// join the parts to a query-string url-component
var queryString = parts.map(function (varInfo) {
return varInfo.path + '=' + varInfo.val;
}).join('&');
if (urlEncode) return encodeURIComponent(queryString);else return queryString;
}
Use like:
console.log(toQueryString({
prop1: 'x',
prop2: {
y: 1,
z: 2
}
}, false));
Which outputs:
prop1=x&prop2[y]=1&prop2[z]=2

Here is another non-jQuery version that utilizes lodash or underscore if you're already using one of those libraries:
var toQueryString = function(obj) {
return _.map(obj,function(v,k){
return encodeURIComponent(k) + '=' + encodeURIComponent(v);
}).join('&');
};
^ I wrote that 5 years ago. An updated and more succinct version of this would now (Oct 2019) be:
var input = { cost: 12345, insertBy: 'testUser' };
Object.entries(input)
.map(([k,v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
// cost=12345&insertBy=testUser
Check that the runtime that you're targeting supports Object.entries() or that you're using a transpiler like Babel or TypeScript if it doesn't.

Try the $.param() method:
var result = $.param({ cost: 12345, insertBy: 'testUser' });

General JavaScript:
function toParam(obj) {
var str = "";
var seperator = "";
for (key in obj) {
str += seperator;
str += enncodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
seperator = "&";
}
return str;
}
toParam({ cost: 12345, insertBy: 'testUser' })
"cost=12345&insertBy=testUser"

Another version:
function toQueryString(obj) {
return Object.keys(obj).map(k => {
return encodeURIComponent(k) + "=" + encodeURIComponent(obj[k])
})
.join("&");
}

var myObj = { cost: 12345, insertBy: 'testUser' },
param = '',
url = 'http://mysite.com/mypage.php';
for (var p in myObj) {
if (myObj.hasOwnProperty(p)) {
param += encodeURIComponent(p) + "=" + encodeURIComponent(myObj[p]) + "&";
}
}
window.location.href = url + "?" + param;

you can use this
function serialize(obj)
{
let str = []
for(var p in obj)
{
if(obj.hasOwnProperty(p)) str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]))
}
return str.join('&')
}
try on JSFiddle on this link https://jsfiddle.net/yussan/kwmnkca6/

ES6 version of Jrop's answer (also parses nested params)
const toQueryString = (obj, urlEncode = false) => {
if (!obj) return null;
const flattenObj = (x, path = []) => {
const result = [];
Object.keys(x).forEach((key) => {
if (!Object.prototype.hasOwnProperty.call(x, key)) return;
const newPath = path.slice();
newPath.push(key);
let vals = [];
if (typeof x[key] === 'object') {
vals = flattenObj(x[key], newPath);
} else {
vals.push({ path: newPath, val: x[key] });
}
vals.forEach((v) => {
return result.push(v);
});
});
return result;
};
let parts = flattenObj(obj);
parts = parts.map((varInfo) => {
if (varInfo.path.length === 1) {
varInfo.path = varInfo.path[0]; // eslint-disable-line no-param-reassign
} else {
const first = varInfo.path[0];
const rest = varInfo.path.slice(1);
varInfo.path = `${first}[${rest.join('][')}]`; // eslint-disable-line no-param-reassign
}
return varInfo;
});
const queryString = parts.map((varInfo) => {
return `${varInfo.path}=${varInfo.val}`;
}).join('&');
if (urlEncode) {
return encodeURIComponent(queryString);
}
return queryString;
};

Using Lodash library it can be done as:
let data = {}
_.map(data, (value, key) => `${key}=${value}`)
.join("&");
Note that this library has been imported as:
window._ = require('lodash');

Related

Generated a Nested Object from TLV

I am trying to parse a TLV string and need to generate a nested object from that string.
const text = 'AA06ClaireCC04JackBB03TomEE05James'
The output needs to look like this:
"Acrobatic Artist": {
"AA": {
"Claire": {
"Curious Camper": {
"CC": {
"Jack": {
"Baddest Banana": {
"BB": {
"Tom": {
"Energetic Elephant": {
"EE": {
"James" : "LASTRECORD"
}
}
}
}
}
}
}
}
}
}
}
Here is what I currently have:
const map = {
AA: 'Acrobatic Artist',
BB: 'Baddest Banana',
CC: 'Curious Camper',
DD: 'Desperate Driver',
EE: 'Energetic Elephant'
}
function createJson(str) {
let json = {}
let remainingText = str
while(remainingText.length > 0) {
const tag = remainingText.substring(0, 2)
const len = remainingText.substring(2, 4)
const val = remainingText.substring(4, len)
const offset = tag.length + len.length + parseInt(len, 16)
remainingText = remainingText.substring(offset)
console.log('new text: ' + remainingText)
json[map[tag]] = {}
json[map[tag]][tag] = {}
json[map[tag]][tag][val] = {}
}
return json
}
But this just creates an object that looks like this:
{
Acrobatic Artist: {
AA: {
Claire: {}
}
},
Baddest Banana: {
BB: {
Tom: {}
}
},
Curious Camper: {
CC: {
Jack: {}
}
},
Energetic Elephant: {
EE: {
James: {}
}
}
}
Here is my fiddle:
https://jsfiddle.net/kzaiwo/y9m2h60t/8/
Note:
Please disregard the LASTRECORD part. I just added that to complete the key-value pair (for the last pair) in the above example. Thank you!
Thanks!
If you keep a reference to a prev value, which starts off as the original json object, you can then continuously update it and its children. When you're updating your object within the while loop you can update prev, and set it to the last child object that you create so that on the next iteration of your loop that particular child object will be updated to contain the new key-value pairs.
const map = {
AA: 'Acrobatic Artist',
BB: 'Baddest Banana',
CC: 'Curious Camper',
DD: 'Desperate Driver',
EE: 'Energetic Elephant'
};
const text = 'AA06ClaireCC04JackBB03TomEE05James';
function createJson(str) {
let json = {};
let prev = json;
let remainingText = str;
while (remainingText.length > 0) {
const tag = remainingText.substring(0, 2);
const len = remainingText.substring(2, 4);
const val = remainingText.substring(4, 4 + parseInt(len, 16));
const offset = tag.length + len.length + parseInt(len, 16);
remainingText = remainingText.substring(offset);
prev[map[tag]] = {};
prev[map[tag]][tag] = {};
prev = prev[map[tag]][tag][val] = {};
}
return json;
}
console.log(createJson(text));
Given the regular structure of your string (2-character code + 2-character number + characters), you can use a simple regex to split out the various parts.
From there you can (flat) map each section into an array of keys.
Finally, you can reduce-right the array to produce the result you want.
const map = {AA:"Acrobatic Artist",BB:"Baddest Banana",CC:"Curious Camper",DD:"Desperate Driver",EE:"Energetic Elephant"};
const text = "AA06ClaireCC04JackBB03TomEE05James";
// Parses a code, length and value from the start of the provided string
const parseSection = (str) => {
const [, code, valueLength] = str.match(/^(\w{2})([a-fA-F0-9]{2})/);
const length = parseInt(valueLength, 16) + 4;
return {
code,
length,
type: map[code],
value: str.slice(4, length),
};
};
// Iterates through the string, extracting sections until finished
const parseTlv = (str) => {
const sections = [];
while (str.length) {
const section = parseSection(str);
sections.push(section);
str = str.slice(section.length);
}
return sections;
};
// Map each section to a flat array of keys then reduce-right to form
// a tree structure
const lastRecord = {};
const result = parseTlv(text)
.flatMap(({ type, code, value }) => [type, code, value])
.reduceRight(
(obj, key) => ({
[key]: obj,
}),
lastRecord
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; }
Here is a two part solution:
for() loop: create an array of items based on <tag><len><val> patterns
.reduce(): build the nested object from the items array
const input = 'AA06ClaireCC04JackBB03TomEE05James';
const tagMap = {
AA: 'Acrobatic Artist',
BB: 'Baddest Banana',
CC: 'Curious Camper',
DD: 'Desperate Driver',
EE: 'Energetic Elephant'
};
let items = [];
for(let i = 0; i < input.length; ) {
let tag = input.substring(i, i + 2);
let len = parseInt(input.substring(i + 2, i + 4), 16);
let val = input.substring(i + 4, i + 4 + len);
items.push([tag, val]);
i += 4 + len;
}
let result = {};
items.reduce((obj, arr) => {
const tag = arr[0];
const val = arr[1];
const name = tagMap[tag] || 'unknown';
//console.log(name, tag, val);
[name, tag, val].forEach(key => {
obj[key] = {};
obj = obj[key];
});
return obj;
}, result);
console.log(result);
Output:
{
"Acrobatic Artist": {
"AA": {
"Claire": {
"Curious Camper": {
"CC": {
"Jack": {
"Baddest Banana": {
"BB": {
"Tom": {
"Energetic Elephant": {
"EE": {
"James": {}
}
}
}
}
}
}
}
}
}
}
}
}
Note: the resulting object has an empty {} as the innermost value; you could replace that with "LASTRECORD" if needed

JavaScript: Create object with nested properties from string split by specific character

How to use the name property in this object:
const obj = {
name: 'root/branch/subbranch/leaf',
value: 'my-value'
}
To create an object with the following format:
{
root: {
branch: {
subbranch: {
leaf: 'my-value'
}
}
}
}
You could do this using split and reduce
const obj = {
name: 'root/branch/subbranch/leaf',
value: 'my-value'
}
let newObj = {}
const parts = obj.name.split('/')
parts.reduce((prev, curr, i) => (
Object.assign(
prev,
{[curr]: i === parts.length - 1 ? obj.value : Object(prev[curr])}
),
prev[curr]
), newObj)
console.log(newObj)
I wrote a reciursive function and a wrapper to call it.
const obj = {
name: 'root/branch/subbranch/leaf',
value: 'my-value'
}
const recursiveNest = (result, value, arr, index = 0) => {
const path = arr[index]
if (index < arr.length - 1) {
result[path] = {}
index +=1;
recursiveNest(result[path], value, arr, index)
} else {
result[arr[index]] = value;
}
};
const createNestedObject = (obj, splitBy) => {
let result = {}
recursiveNest(result, obj.value, obj.name.split(splitBy))
return result;
}
console.log(createNestedObject(obj, '/'));
Lodash provides setWith(object, path, value, [customizer]):
const obj = {
name: 'root/branch/subbranch/leaf',
value: 'my-value'
}
console.log(_.setWith({}, obj.name.split('/'), obj.value, _.stubObject))
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>

Modify object in Javascript

I'm trying to convert object
var data = {"USD": 12323,"CAD":32123}
to become
[{"id":"USD","value":12323},{"id":"CAD","value":32123}]
This is what i tried so far
var res = Object.keys(data).map(function(k) {
return [k, result[k]];
});
and get the result
[["USD", 12323],["CAD", 32123]]
Any help is very appreciated.
Thanks,
Just change your return statement to
return {id: k, value:result[k]}; //observe that an object is returned instead of an array
Demo
var data = {"USD": 12323,"CAD":32123};
var res = Object.keys(data).map(function(k) {
return {id: k, value:data[k]};
});
console.log( res );
You're creating one array, not an object. Change [] to {}:
var res = {"USD": 12323,"CAD":32123};
res = Object.keys(res).map(function(k) {
return {id: k, value:res[k]};
});
console.log(res);
as an alternative, you can use arrow function:
var res = {"USD": 12323,"CAD":32123};
res = Object.keys(res).map(x => x = { id: x, value: res[x]});
console.log(res);
or .every():
var res = {"USD": 12323,"CAD":32123};
var resWArrow = Object.entries(res).map(function(k) {
return {id: k[0], value: k[1]};
});
console.log('Without arrow: ' + JSON.stringify(resWArrow));
var resArrow = Object.entries(res).map(x => x = { id: x[0], value: x[1] });
console.log('With arrow: ' + JSON.stringify(resArrow));
With ES6, you could take Object.entries with Array#map and a destructuring assignment for the elements and short hand properties for the result.
var data = { "USD": 12323, "CAD": 32123 },
result = Object.entries(data).map(([id, value]) => ({ id, value }));
console.log(result);
You can iterate over the keys of the object and map them like follows:
Object.keys(res).map(function(key) {
return {id: key, value: data[key]};
}
Change the return statement and return an object
var res = {
"USD": 12323,
"CAD": 32123
}
var m = Object.keys(res).map(function(item) {
return {
id: item,
value: res[item]
}
})
console.log(m)

Removing object properties with Lodash

I have to remove unwanted object properties that do not match my model. How can I achieve it with Lodash?
My model is:
var model = {
fname: null,
lname: null
}
My controller output before sending to the server will be:
var credentials = {
fname: "xyz",
lname: "abc",
age: 23
}
I am aware I can use
delete credentials.age
but what if I have lots of unwanted properties? Can I achieve it with Lodash?
You can approach it from either an "allow list" or a "block list" way:
// Block list
// Remove the values you don't want
var result = _.omit(credentials, ['age']);
// Allow list
// Only allow certain values
var result = _.pick(credentials, ['fname', 'lname']);
If it's reusable business logic, you can partial it out as well:
// Partial out a "block list" version
var clean = _.partial(_.omit, _, ['age']);
// and later
var result = clean(credentials);
Note that Lodash 5 will drop support for omit
A similar approach can be achieved without Lodash:
const transform = (obj, predicate) => {
return Object.keys(obj).reduce((memo, key) => {
if(predicate(obj[key], key)) {
memo[key] = obj[key]
}
return memo
}, {})
}
const omit = (obj, items) => transform(obj, (value, key) => !items.includes(key))
const pick = (obj, items) => transform(obj, (value, key) => items.includes(key))
// Partials
// Lazy clean
const cleanL = (obj) => omit(obj, ['age'])
// Guarded clean
const cleanG = (obj) => pick(obj, ['fname', 'lname'])
// "App"
const credentials = {
fname:"xyz",
lname:"abc",
age:23
}
const omitted = omit(credentials, ['age'])
const picked = pick(credentials, ['age'])
const cleanedL = cleanL(credentials)
const cleanedG = cleanG(credentials)
Get a list of properties from model using _.keys(), and use _.pick() to extract the properties from credentials to a new object:
var model = {
fname:null,
lname:null
};
var credentials = {
fname:"xyz",
lname:"abc",
age:23
};
var result = _.pick(credentials, _.keys(model));
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.4/lodash.min.js"></script>
If you don't want to use Lodash, you can use Object.keys(), and Array.prototype.reduce():
var model = {
fname:null,
lname:null
};
var credentials = {
fname:"xyz",
lname:"abc",
age:23
};
var result = Object.keys(model).reduce(function(obj, key) {
obj[key] = credentials[key];
return obj;
}, {});
console.log(result);
You can easily do this using _.pick:
var model = {
fname: null,
lname: null
};
var credentials = {
fname: 'abc',
lname: 'xyz',
age: 2
};
var result = _.pick(credentials, _.keys(model));
console.log('result =', result);
<script src="https://cdn.jsdelivr.net/lodash/4.16.4/lodash.min.js"></script>
But you can simply use pure JavaScript (specially if you use ECMAScript 6), like this:
const model = {
fname: null,
lname: null
};
const credentials = {
fname: 'abc',
lname: 'xyz',
age: 2
};
const newModel = {};
Object.keys(model).forEach(key => newModel[key] = credentials[key]);
console.log('newModel =', newModel);
Lodash unset is suitable for removing a few unwanted keys.
const myObj = {
keyOne: "hello",
keyTwo: "world"
}
unset(myObj, "keyTwo");
console.log(myObj); /// myObj = { keyOne: "hello" }
Here I have used omit() for the respective 'key' which you want to remove... by using the Lodash library:
var credentials = [{
fname: "xyz",
lname: "abc",
age: 23
}]
let result = _.map(credentials, object => {
return _.omit(object, ['fname', 'lname'])
})
console.log('result', result)
You can use _.omit() for emitting the key from a JSON array if you have fewer objects:
_.forEach(data, (d) => {
_.omit(d, ['keyToEmit1', 'keyToEmit2'])
});
If you have more objects, you can use the reverse of it which is _.pick():
_.forEach(data, (d) => {
_.pick(d, ['keyToPick1', 'keyToPick2'])
});
To select (or remove) object properties that satisfy a given condition deeply, you can use something like this:
function pickByDeep(object, condition, arraysToo=false) {
return _.transform(object, (acc, val, key) => {
if (_.isPlainObject(val) || arraysToo && _.isArray(val)) {
acc[key] = pickByDeep(val, condition, arraysToo);
} else if (condition(val, key, object)) {
acc[key] = val;
}
});
}
https://codepen.io/aercolino/pen/MWgjyjm
This is my solution to deep remove empty properties with Lodash:
const compactDeep = obj => {
const emptyFields = [];
function calculateEmpty(prefix, source) {
_.each(source, (val, key) => {
if (_.isObject(val) && !_.isEmpty(val)) {
calculateEmpty(`${prefix}${key}.`, val);
} else if ((!_.isBoolean(val) && !_.isNumber(val) && !val) || (_.isObject(val) && _.isEmpty(val))) {
emptyFields.push(`${prefix}${key}`);
}
});
}
calculateEmpty('', obj);
return _.omit(obj, emptyFields);
};
For array of objects
model = _.filter(model, a => {
if (!a.age) { return a }
})
Recursively removing paths.
I just needed something similar, not removing just keys, but keys by with paths recursively.
Thought I'd share.
Simple readable example, no dependencies
/**
* Removes path from an object recursively.
* A full path to the key is not required.
* The original object is not modified.
*
* Example:
* const original = { a: { b: { c: 'value' } }, c: 'value' }
*
* omitPathRecursively(original, 'a') // outputs: { c: 'value' }
* omitPathRecursively(original, 'c') // outputs: { a: { b: {} } }
* omitPathRecursively(original, 'b.c') // { a: { b: {} }, c: 'value' }
*/
export const omitPathRecursively = (original, path, depth = 1) => {
const segments = path.split('.')
const final = depth === segments.length
return JSON.parse(
JSON.stringify(original, (key, value) => {
const match = key === segments[depth - 1]
if (!match) return value
if (!final) return omitPathRecursively(value, path, depth + 1)
return undefined
})
)
}
Working example: https://jsfiddle.net/webbertakken/60thvguc/1/
While looking for a solution that would work for both arrays and objects, I didn't find one and so I created it.
/**
* Recursively ignore keys from array or object
*/
const ignoreKeysRecursively = (obj, keys = []) => {
const keyIsToIgnore = (key) => {
return keys.map((a) => a.toLowerCase()).includes(key)
}
const serializeObject = (item) => {
return Object.fromEntries(
Object.entries(item)
.filter(([key, value]) => key && value)
.reduce((prev, curr, currIndex) => {
if (!keyIsToIgnore(curr[0]))
prev[currIndex] =
[
curr[0],
// serialize array
Array.isArray(curr[1])
? // eslint-disable-next-line
serializeArray(curr[1])
: // serialize object
!Array.isArray(curr[1]) && typeof curr[1] === 'object'
? serializeObject(curr[1])
: curr[1],
] || []
return prev
}, []),
)
}
const serializeArray = (item) => {
const serialized = []
for (const entry of item) {
if (typeof entry === 'string') serialized.push(entry)
if (typeof entry === 'object' && !Array.isArray(entry)) serialized.push(serializeObject(entry))
if (Array.isArray(entry)) serialized.push(serializeArray(entry))
}
return serialized
}
if (Array.isArray(obj)) return serializeArray(obj)
return serializeObject(obj)
}
// usage
const refObject = [{name: "Jessica", password: "ygd6g46"}]
// ignore password
const obj = ignoreKeysRecursively(refObject, ["password"])
// expects returned array to only have name attribute
console.log(obj)
let asdf = [{"asd": 12, "asdf": 123}, {"asd": 121, "asdf": 1231}, {"asd": 142, "asdf": 1243}]
asdf = _.map(asdf, function (row) {
return _.omit(row, ['asd'])
})

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