Is there a known way or a library that already has a helper for assessing whether an object is serializable in JavaScript?
I tried the following but it doesn't cover prototype properties so it provides false positives:
_.isEqual(obj, JSON.parse(JSON.stringify(obj))
There's another lodash function that might get me closer to the truth, _.isPlainObject. However, while _.isPlainObject(new MyClass()) returns false, _.isPlainObject({x: new MyClass()}) returns true, so it needs to be applied recursively.
Before I venture by myself on this, does anybody know an already reliable way for checking if JSON.parse(JSON.stringify(obj)) will actually result in the same object as obj?
function isSerializable(obj) {
var isNestedSerializable;
function isPlain(val) {
return (typeof val === 'undefined' || typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number' || Array.isArray(val) || _.isPlainObject(val));
}
if (!isPlain(obj)) {
return false;
}
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (!isPlain(obj[property])) {
return false;
}
if (typeof obj[property] == "object") {
isNestedSerializable = isSerializable(obj[property]);
if (!isNestedSerializable) {
return false;
}
}
}
}
return true;
}
Recursively iterating over all of given object properties. They can be either:
plain objects ("an object created by the Object constructor or one with a [[Prototype]] of null." - from lodash documentation)
arrays
strings, numbers, booleans
undefined
Any other value anywhere within passed obj will cause it to be understood as "un-serializable".
(To be honest I'm not absolutely positive that I didn't omit check for some serializable/non-serializable data types, which actually I think depends on the definition of "serializable" - any comments and suggestions will be welcome.)
In the end I created my own method that leverages Underscore/Lodash's _.isPlainObject. My function ended up similar to what #bardzusny proposed, but I'm posting mine as well since I prefer the simplicity/clarity. Feel free to outline pros/cons.
var _ = require('lodash');
exports.isSerializable = function(obj) {
if (_.isUndefined(obj) ||
_.isNull(obj) ||
_.isBoolean(obj) ||
_.isNumber(obj) ||
_.isString(obj)) {
return true;
}
if (!_.isPlainObject(obj) &&
!_.isArray(obj)) {
return false;
}
for (var key in obj) {
if (!exports.isSerializable(obj[key])) {
return false;
}
}
return true;
};
Here is a slightly more Lodashy ES6 version of #treznik solution
export function isSerialisable(obj) {
const nestedSerialisable = ob => (_.isPlainObject(ob) || _.isArray(ob)) &&
_.every(ob, isSerialisable);
return _.overSome([
_.isUndefined,
_.isNull,
_.isBoolean,
_.isNumber,
_.isString,
nestedSerialisable
])(obj)
}
Tests
describe.only('isSerialisable', () => {
it('string', () => {
chk(isSerialisable('HI'));
});
it('number', () => {
chk(isSerialisable(23454))
});
it('null', () => {
chk(isSerialisable(null))
});
it('undefined', () => {
chk(isSerialisable(undefined))
});
it('plain obj', () => {
chk(isSerialisable({p: 1, p2: 'hi'}))
});
it('plain obj with func', () => {
chkFalse(isSerialisable({p: 1, p2: () => {}}))
});
it('nested obj with func', () => {
chkFalse(isSerialisable({p: 1, p2: 'hi', n: { nn: { nnn: 1, nnm: () => {}}}}))
});
it('array', () => {
chk(isSerialisable([1, 2, 3, 5]))
});
it('array with func', () => {
chkFalse(isSerialisable([1, 2, 3, () => false]))
});
it('array with nested obj', () => {
chk(isSerialisable([1, 2, 3, { nn: { nnn: 1, nnm: 'Hi'}}]))
});
it('array with newsted obj with func', () => {
chkFalse(isSerialisable([1, 2, 3, { nn: { nnn: 1, nnm: () => {}}}]))
});
});
}
Here's how this can be achieved without relying on 3rd party libraries.
We would usually think of using the typeof operator for this kind of task, but it can't be trusted on its own, otherwise we end up with nonsense like:
typeof null === "object" // true
typeof NaN === "number" // true
So the first thing we need to do is find a way to reliably detect the type of any value (Taken from MDN Docs):
const getTypeOf = (value: unknown) => {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
};
We can then traverse the object or array (if any) recursively and check if the deserialized output matches the input type at every step:
const SERIALIZATION_ERROR = new Error(
`the input value could not be serialized`
);
const serialize = (input: unknown) => {
try {
const serialized = JSON.stringify(input);
const inputType = getTypeOf(input);
const deserialized = JSON.parse(serialized);
const outputType = getTypeOf(parsed);
if (outputType !== inputType) throw SERIALIZATION_ERROR;
if (inputType === "object") {
Object.values(input as Record<string, unknown>).forEach((value) =>
serialize(value)
);
}
if (inputType === "array") {
(input as unknown[]).forEach((value) => serialize(value));
}
return serialized;
} catch {
throw SERIALIZATION_ERROR;
}
};
Here's my solution with vanilla JS using pattern matching. It correctly flags Symbol() keys as non-serializable, a problem I ran into with the other code listed here.
It's also nicely concise, and maybe a bit more readable.
Returns true if the parameters can be serialized to JSON, returns false otherwise.
const isSerializable = n => (({
[ !!"default" ]: () => false,
[ typeof n === "boolean" ]: () => true,
[ typeof n === "string" ]: () => true,
[ typeof n === "number" ]: () => true,
[ typeof n === "object" ]: () =>
! Object.getOwnPropertySymbols( n ).length &&
isSerializable( Object.entries( n ) ),
[ Array.isArray( n ) ]: () => ! n.some( n => ! isSerializable( n ) ),
[ n === null ]: () => true,
})[ true ])();
Related
What is a better way of doing this. I'am assigning either of two property values (from two different objects), depending on their existence, to a third data-structure.
In case the args object's value is nullish a non nullish value gets accessed from the default object and assigned to the final structure.
return {
first: {
visible: args.first?.visible ?? defaulttest.first?.visible,
emoji: args.first?.emoji ?? defaulttest.first?.emoji,
style: args.first?.style ?? defaulttest.first?.style,
},
back: {
visible: args.back?.visible ?? defaulttest.back?.visible,
emoji: args.back?.emoji ?? defaulttest.back?.emoji,
style: args.back?.style ?? defaulttest.back?.style,
},
page: {
visible: args.page?.visible ?? defaulttest.page?.visible,
emoji: args.page?.emoji ?? defaulttest.page?.emoji,
style: args.page?.style ?? defaulttest.page?.style,
},
forward: {
visible: args.forward?.visible ?? defaulttest.forward?.visible,
emoji: args.forward?.emoji ?? defaulttest.forward?.emoji,
style: args.forward?.style ?? defaulttest.forward?.style,
},
last: {
visible: args.last?.visible ?? defaulttest.last?.visible,
emoji: args.last?.emoji ?? defaulttest.last?.emoji,
style: args.last?.style ?? defaulttest.last?.style,
},
Mdelete: {
visible: args.Mdelete?.visible ?? defaulttest.Mdelete?.visible,
emoji: args.Mdelete?.emoji ?? defaulttest.Mdelete?.emoji,
style: args.Mdelete?.style ?? defaulttest.Mdelete?.style,
},
removeBtn: {
visible: args.removeBtn?.visible ?? defaulttest.removeBtn?.visible,
emoji: args.removeBtn?.emoji ?? defaulttest.removeBtn?.emoji,
style: args.removeBtn?.style ?? defaulttest.removeBtn?.style,
},
};
From my above comments ...
1/2 ... The OP actually is not really comparing. For a certain set of properties the OP looks up each property at a target object, and only in case it features a nullish value there will be an assignment from a source object's counterpart to the missing property. Thus an approach I would choose was ...
2/2 ... implementing a generic function which merges two objects in a way that a source property can only be written/assigned in case the target structure does not already provide a non nullish value. This function then has to be invoked twice once for args and defaulttest and a second time for the to be returned entirely empty data structure and args.
The above statement was a bit ambitious for there are at least 2 strategies of how one could achieve such kind of mergers.
Thus the below provided example code implements two approaches
one, called refit, which follows a pushing/patching agenda due to forcing the assignement of every non nullish property in a source-object to its non nullish counterpart of a target-object.
a 2nd one, called revive, which resembles a pulling approach for it just reassigns the nullish target-object properties with their non nullish source-object counterparts.
The difference in the results they produce for one and the same preset is going to be demonstrated herby ...
// "refit" ... a pushing/patching approach.
// - force the assignement of every non nullish property in source
// to its non nullish counterpart in target ... hence a *refit*.
function refitNullishValuesRecursively(target, source) {
if (
// are both values array-types?
Array.isArray(source) &&
Array.isArray(target)
) {
source
// for patching always iterate the source items ...
.forEach((sourceItem, idx) => {
// ... and look whether a target counterpart exists.
if (target[idx] == null) {
// either assign an existing structured clone ...
if (sourceItem != null) {
target[idx] = cloneDataStructure(sourceItem);
}
} else {
// ... or proceed recursively.
refitNullishValuesRecursively(target[idx], sourceItem);
}
});
} else if (
// are both values object-types?
source && target &&
'object' === typeof source &&
'object' === typeof target
) {
Object
// for patching ...
.entries(source)
// ... always iterate the source entries (key value pairs) ...
.forEach(([key, sourceValue], idx) => {
// ... and look whether a target counterpart exists.
if (target[key] == null) {
// either assign an existing structured clone ...
if (sourceValue != null) {
target[key] = cloneDataStructure(sourceValue);
}
} else {
// ... or proceed recursively.
refitNullishValuesRecursively(target[key], sourceValue);
}
});
}
return target;
}
// "revive" ... a pulling approach.
// - just reassign the nullish target properties with their
// non nullish source counterparts ... hence a *revive*.
function reviveNullishValuesRecursively(target, source) {
if (
// are both values array-types?
Array.isArray(target) &&
Array.isArray(source)
) {
target
// for fixing always iterate the target items.
.forEach((targetItem, idx) => {
if (targetItem == null) {
// either assign an existing structured clone ...
target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
} else {
// ... or proceed recursively.
reviveNullishValuesRecursively(targetItem, source[idx]);
}
});
} else if (
// are both values object-types?
target && source &&
'object' === typeof target &&
'object' === typeof source
) {
Object
// for fixing ...
.entries(target)
// ... always iterate the target entries (key value pairs).
.forEach(([key, targetValue], idx) => {
if (targetValue == null) {
// either assign an existing structured clone ...
target[key] = cloneDataStructure(source[key]) ?? targetValue;
} else {
// ... or proceed recursively.
reviveNullishValuesRecursively(targetValue, source[key]);
}
});
}
return target;
}
const cloneDataStructure =
('function' === typeof structuredClone)
&& structuredClone
|| (value => JSON.parse(JSON.stringify(value)));
const targetBlueprint = {
x: { xFoo: 'foo', xBar: 'bar', xBaz: { xBiz: null } },
y: { yFoo: 'foo', yBar: null },
};
const patch = {
x: { xFoo: null, xBar: null, xBaz: { xBiz: 'biz' } },
y: { yFoo: null, yBar: 'bar', yBaz: { yBiz: 'biz' } },
};
let target = cloneDataStructure(targetBlueprint);
console.log('"refit" ... a pushing/patching approach.');
console.log('before refit ...', { target, patch });
refitNullishValuesRecursively(target, patch);
console.log('after refit ...', { target, patch });
target = cloneDataStructure(targetBlueprint);
console.log('"revive" ... a pulling approach.');
console.log('before revive ...', { target, patch });
reviveNullishValuesRecursively(target, patch);
console.log('after revive ...', { target, patch });
.as-console-wrapper { min-height: 100%!important; top: 0; }
As for the OP's example which targets the creation of kind of a config-object, one could fully patch/refit a clone of the temporary or current args-config, whereas within the last step one has control over the config-object's final structure by providing the most basic empty config-base which just gets revived by the before created full patch/refit-config.
function refitNullishValuesRecursively(target, source) {
if (Array.isArray(source) && Array.isArray(target)) {
source
.forEach((sourceItem, idx) => {
if (target[idx] == null) {
if (sourceItem != null) {
target[idx] = cloneDataStructure(sourceItem);
}
} else {
refitNullishValuesRecursively(target[idx], sourceItem);
}
});
} else if (
source && target &&
'object' === typeof source &&
'object' === typeof target
) {
Object
.entries(source)
.forEach(([key, sourceValue], idx) => {
if (target[key] == null) {
if (sourceValue != null) {
target[key] = cloneDataStructure(sourceValue);
}
} else {
refitNullishValuesRecursively(target[key], sourceValue);
}
});
}
return target;
}
function reviveNullishValuesRecursively(target, source) {
if (Array.isArray(target) && Array.isArray(source)) {
target
.forEach((targetItem, idx) => {
if (targetItem == null) {
target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
} else {
reviveNullishValuesRecursively(targetItem, source[idx]);
}
});
} else if (
target && source &&
'object' === typeof target &&
'object' === typeof source
) {
Object
.entries(target)
.forEach(([key, targetValue], idx) => {
if (targetValue == null) {
target[key] = cloneDataStructure(source[key]) ?? targetValue;
} else {
reviveNullishValuesRecursively(targetValue, source[key]);
}
});
}
return target;
}
const cloneDataStructure =
('function' === typeof structuredClone)
&& structuredClone
|| (value => JSON.parse(JSON.stringify(value)));
const defaultConfig = {
first: {
visible: 'default.first.visible',
emoji: 'default.first.emoji',
style: 'default.first.style',
},
forward: {
visible: 'default.forward.visible',
emoji: 'default.forward.emoji',
style: 'default.forward.style',
},
removeBtn: {
visible: 'default.removeBtn.visible',
emoji: 'default.removeBtn.emoji',
style: 'default.removeBtn.style',
},
};
const currentConfig = {
first: {
visible: 'current.first.visible',
emoji: 'current.first.emoji',
style: 'current.first.style',
},
forward: {
visible: 'current.forward.visible',
emoji: null,
},
FOO: {
visible: 'current.FOO.visible',
emoji: 'current.FOO.emoji',
style: 'current.FOO.style',
}
};
function getConfiguration(baseConfig) {
return reviveNullishValuesRecursively(
cloneDataStructure(baseConfig),
refitNullishValuesRecursively(
cloneDataStructure(currentConfig),
defaultConfig,
),
);
}
console.log(
getConfiguration({
first: null,
forward: null,
removeBtn: null,
})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
If the structure of your object is the one you presented you can do:
function normalize(input, defaultValue) {
// Loop on the outer keys
Object.keys(input).forEach(mainKey => {
// Loop on the inner keys
Object.keys(input[mainKey]).forEach(key => {
// set the value of the key as itself or default if null
input[mainKey][key] = input[mainKey]?.[key] ?? defaultValue[mainKey]?.[key]
})
})
return input;
}
Calling normalize(args, defaulttest) you will loop on each inner key, check if it exist and if it does not exist you substitute it with the default in the same path.
Example:
const x = {
a: {a1: '1', a2: '2'},
b: {b1: '1', b2: null}
}
const y = {b: {b2: '5'}}
console.log(normalize(x,y))
Output:
{
"a": {
"a1": "1",
"a2": "2"
},
"b": {
"b1": "1",
"b2": "5"
}
}
With this approach you must have the key in the args input. If the key is missing, it will not be substituted with the default. To make it work even with not-present keys you need to use a third structure with all the possible path for example.
I have a string like this "(ReadAccountAccess || ReadContractAccess) && CreateAccountAccess"
And I have this object:
{
PermissionID: 1,
AccountID: 1,
ReadAccountAccess: true,
ReadContractAccess: true,
CreateAccountAccess: true,
}
How can I check this condition?
It is NOT recommended, but you could use with statement and evaluate the expression with that object as the scope.
function check(obj, condition) {
with(obj) {
return eval(condition)
}
}
const input={PermissionID:1,AccountID:1,ReadAccountAccess:true,ReadContractAccess:true,CreateAccountAccess:true};
console.log(
check(input, "(ReadAccountAccess || ReadContractAccess) && CreateAccountAccess"),
check(input, "ReadAccountAccess === false"),
check(input, "AccountID === 1")
)
Instead of saving the condition in a string, you could use a function which takes an object as an input and returns the condition expression. This is a much better of deferring the check.
const input={PermissionID:1,AccountID:1,ReadAccountAccess:true,ReadContractAccess:true,CreateAccountAccess:true};
const c1 = o => (o.ReadAccountAccess || o.ReadContractAccess) && o.CreateAccountAccess,
c2 = o => o.ReadAccountAccess === false,
c3 = o => o.AccountID === 1
console.log(
c1(input),
c2(input),
c3(input)
)
You can use eval() javascript function.
const obj = {
PermissionID: 1,
AccountID: 1,
ReadAccountAccess: true,
ReadContractAccess: true,
CreateAccountAccess: true,
};
const { PermissionID, AccountID, ReadAccountAccess, ReadContractAccess, CreateAccountAccess } = obj;
const opr = "(ReadAccountAccess || ReadContractAccess) && CreateAccountAccess";
console.log(eval(opr));
What I am trying to achieve here is that, I made a simple utility function and that should return 'true' or 'false', based on the given argument to the isEmpty method.
// The below log should return 'false', But it's returning 'true'
console.log(isEmpty( () => {key: 1} ));
What I tried so far
function isEmpty(value) {
const type = typeof value;
if ((value !== null && type === 'object') || type === 'function') {
const properties = Object.keys(value);
return properties.length === 0 || properties.size === 0
}
return !value;
}
And it's working for below cases
console.log(isEmpty( {} )) // true
console.log(isEmpty( [] )) // true
console.log(isEmpty( Object.create(null) )) // true
console.log(isEmpty( null )) // true
console.log(isEmpty( '' )) // true
console.log(isEmpty( {key: 1} )) // false
console.log(isEmpty( [1,2,3] )) // false
But it's not working, when we get the return object / array from the function
console.log(isEmpty( () => ({key: 1}) ))
console.log(isEmpty( () => ([1,2,3]) ))
Using Object.keys() on a function will always result in an empty array (no matter the return type). This is due to the fact that the keys of a function are not enumerable (the keys being name and length), and Object.keys() will only give back the keys which can be enumerated. This means the length of the array of keys will always be 0, meaning your function will give back true even though the function it is passed returns a non-empty value.
If you can invoke the value (if it is a function), it will allow you to get the returned value out of it (ie: an object or array), and then recurse with your function that you currently have:
function isEmpty(value) {
const type = typeof value;
if (value !== null && type === 'object') {
const prototype = Object.getPrototypeOf(value) || {};
const properties = Object.keys(value) + Object.keys(prototype);
return properties.length === 0 || properties.size === 0
} else if(type === 'function') {
const res = value();
return isEmpty(res);
}
return !value;
}
console.log(isEmpty( {} )) // true
console.log(isEmpty( [] )) // true
console.log(isEmpty( Object.create(null) )) // true
console.log(isEmpty( null )) // true
console.log(isEmpty( '' )) // true
console.log(isEmpty(() => ({}))); // true
console.log(isEmpty(() => () => () => ({}))); // true
console.log(isEmpty( {key: 1} )) // false
console.log(isEmpty( [1,2,3] )) // false
console.log(isEmpty(() => ({key: 1}))); // false
console.log(isEmpty( () => ([1,2,3]) )) // false
console.log(isEmpty(() => (Object.create({key: 1})))) // false
In order to achieve that you will have to check the type of the value and if it is a function then invoke to get the result.
If the function passed have side effects they would be called, which could cause some issues.
function isEmpty(value) {
const type = typeof value;
// If passed a function, invoke it and get the result
if (type === 'function') {
value = value();
}
if (value && (type === 'object' || type === 'function')) {
const properties = Object.keys(value);
return properties.length === 0 || properties.size === 0
}
return !value;
}
When you call a function like () => { key: 1 } you are actually creating a function which looks like
function {
key: 1
}
which means that the function do not have a return value. Instead you should use it like this () => ({ key: 1 }) which will create a
function like:
function {
return { key: 1 }
}
https://babeljs.io/repl#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYewdgzgLgBAYgVzMAjDAvDAFASgwPhgG8YBrAUwE8AuGNAXwChRJZFkAmDbPdQrEhRp0Y9HEA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=es2015%2Creact%2Cstage-2&prettier=false&targets=&version=7.7.3&externalPlugins=
I want to find the key of a value in a Javascript nested object with recursion.
Here is my attempt at the function. Are there more elegant ways to implement this?
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' }
function findKey(obj, target) {
let result = null;
if (_.isEmpty(obj) || !_.isObject(obj)){
return null;
}
if (!_.isArray(obj) && Object.keys(obj).length > 0) {
for(let i=0; i < Object.keys(obj).length; i++){
let key = Object.keys(obj)[i];
let val = obj[key];
if (val === target) {
return key;
}else{
result = findKey(val, target);
}
if (result) {break}
}
}
return result;
}
console.log(findKey(foo, 'worked'))
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
For instance is there a way to avoid having to check the value of result to then break?
I feel like result should be able to bubble down the call stack until it returns at the very first function call without having to break.
This was recently brought back up, and one useful technique was not mentioned, generator functions. Often they simplify recursive traversals that need to stop early. Here we break the problem into two functions. One, the generator function nestedEntries gets all the (nested) key-value pairs in the object. The other calls that and returns the first one that matches a target value supplied.
function * nestedEntries (obj) {
for (let [k, v] of Object .entries (obj)) {
yield [k, v]
if (Object (v) === v) {yield * nestedEntries (v)}
}
}
const findKey = (obj, target) => {
for (let [k, v] of nestedEntries (obj)) {
if (v === target) return k
}
return null
}
const foo = {data01: 'rand01', data: {data21: 'rand', data2: { data3: 'worked' } }}
console .log (findKey (foo, 'worked'))
After the few questions made above, it looks like the function should:
Assume the input is always an object.
Assume it might encounter arrays in its way.
Assume it must stop after meeting one value (in case multiple value exists).
The provided input code given by the OP does not handle array cases.
Below code is sampled to work with these sample cases:
Plain nested object structure.
Object with nested arrays of objects or elements.
Below function accepts a second argument which is a callback to evaluate whether the element met is actually the one we're looking for. In this way, it's easier to handle more complex checks.
The recursive approach is kept and, once the key is met, the function simply return to avoid unnecessary searchs.
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' };
const fooWithArrays = {
data: {
data2: {
data3: 'not here'
},
data4: [
{ data5: 'worked' },
{ data6: 'not me' }
]
}
};
const fooWithExpression = {
data: {
data2: {
data3: { id: 15, name: 'find me!' }
},
data21: {
data25: 'not me'
}
}
};
const findKeyByValue = (obj, equalsExpression) => {
// Loop key->value pairs of the input object.
for (var [key, v] of Object.entries(obj)) {
// if the value is an array..
if (Array.isArray(v)) {
// Loop the array.
for (let i = 0; i < v.length; i++) {
// check whether the recursive call returns a result for the nested element.
let res = findKeyByValue(v[i], equalsExpression);
// if so, the key was returned. Simply return.
if (res !== null && res !== undefined) return res;
}
}
// otherwise..
else {
// if the value is not null and not undefined.
if (v !== null && v !== undefined) {
// if the value is an object (typeof(null) would give object, hence the above if).
if (typeof(v) === 'object') {
// check whether the value searched is an object and the match is met.
if (equalsExpression(v)) return key;
// if not, recursively keep searching in the object.
let res = findKeyByValue(v, equalsExpression);
// if the key is found, return it.
if (res !== null && res !== undefined) return res;
}
else {
// finally, value must be a primitive or something similar. Compare.
let res = equalsExpression(v);
// if the condition is met, return the key.
if (res) return key;
// else.. continue.
}
}
else continue;
}
}
}
console.log( findKeyByValue(foo, (found) => found === 'worked') );
console.log( findKeyByValue(fooWithArrays, (found) => found === 'worked') );
console.log( findKeyByValue(fooWithExpression, (found) => found && found.id && found.id === 15) );
You can use Object.entries to iterate all the keys.
Also worth noting, Object.entries also works with Array's, so no
special handling required.
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01', arr: [{arrtest: "arr"},'xyz']}
function findKey(obj, target) {
const fnd = obj => {
for (const [k, v] of Object.entries(obj)) {
if (v === target) return k;
if (typeof v === 'object') {
const f = fnd(v);
if (f) return f;
}
}
}
return fnd(obj);
}
console.log(findKey(foo, 'worked'))
console.log(findKey(foo, 'arr'))
console.log(findKey(foo, 'xyz'))
If obj is exactly a plain object with subobjects without arrays, this does the trick.
function findKey(obj, target) {
for (let key in obj) {
const val = obj[key];
if (val === target) {
return key;
}
if (typeof val === "object" && !Array.isArray(val)) {
const ret = findKey(val, target);
if (ret) return ret;
}
}
}
const foo = {
data: { data2: { data3: "worked" }, data21: "rand" },
data01: "rand01",
};
console.log(findKey(foo, "worked"));
console.log(findKey(foo, "bloop"));
You can try regex, if data is just objects without arrays:
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' }
const out = JSON.stringify(foo).match(/"([^{}]+)":"worked"/)[1];
console.log(out);
For simple data processing tasks like this we use object-scan. It's very powerful once you wrap your head around it and makes things a lot cleaner. Here is how you'd solve your questions
(took the liberty to take the input data from #briosheje answer)
// const objectScan = require('object-scan');
const findKeyByValue = (data, fn) => objectScan(['**'], {
abort: true,
rtn: 'property',
filterFn: ({ value }) => fn(value) === true
})(data);
const foo = { data: { data2: { data3: 'worked' }, data21: 'rand' }, data01: 'rand01' };
const fooWithArrays = { data: { data2: { data3: 'not here' }, data4: [{ data5: 'worked' }, { data6: 'not me' }] } };
const fooWithExpression = { data: { data2: { data3: { id: 15, name: 'find me!' } }, data21: { data25: 'not me' } } };
console.log(findKeyByValue(foo, (found) => found === 'worked'));
// => data3
console.log(findKeyByValue(fooWithArrays, (found) => found === 'worked'));
// => data5
console.log(findKeyByValue(fooWithExpression, (found) => found && found.id && found.id === 15));
// => data3
console.log(findKeyByValue(fooWithExpression, (found) => false));
// => undefined
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
i would like to push keys inside array if found undefined or null
const obj = {
name:'ab',
edu:'av',
degres:{
a1:'',
b1:'1'
},
platform:undefined
}
i want an output like
`['a1','platform']`
as the value for a1 and platform were null and undefined
i have treid this solution but it doesnt work
function iterater(obj){
let blankValues = [];
Object.keys(obj).map((key) => {
if (obj.hasOwnProperty(key) && (typeof obj[key] === "object")) {
iterater(obj[key])
} else {
if (typeof obj[key] === "undefined" || obj[key] === ''){
blankValues.push(key);
}
}
})
return blankValues;
}
but this somehow only return ['platform'] only,but the expected output should be ['platform','a1'],i think when running iterater(obj[key]),the value of array (blankValues) gets blank as it doesnt perserve it,but please help me with appropriate logic and structure
The issue is because you're re-defining blankValues as an empty array in every iteration of the recursive loop. To fix this you could accept the array as an optional argument of the function so that values get pushed to it on each iteration.
Also note that, as #ziggy wiggy pointed out in the comments, your logic will fail when a null value is encountered as typeof obj[key] === "object" would be true. You need a specific null check too.
const obj = {
name: 'ab',
edu: 'av',
degres: {
a1: '',
b1: '1'
},
platform: undefined,
foo: null
}
function iterater(obj, arr) {
arr = arr || [];
Object.keys(obj).map((key) => {
if (obj.hasOwnProperty(key) && (typeof obj[key] === "object") && obj[key] !== null) {
iterater(obj[key], arr)
} else {
if (typeof obj[key] === "undefined" || obj[key] === null || obj[key].trim() === '') {
arr.push(key);
}
}
})
return arr;
}
console.log(iterater(obj));
Note that I also added a trim() call to test the empty string. Your previous logic would accept whitespace-filled strings as valid values.
As you said yourself, when you call iterater(obj[key]) it sets a new local blankValues and puts values in it. So i think you should put blankValues outside the function.
And then you don't have to return it (or you can if you want it as a return value).
Or you can pass blankValues as a parameter of iterater in both the main call and the "inside" call
You need to consume the result of recursive call. For example add it back to blankValues like this blankValues.push(...iterater(obj[key]))
const obj = {
name:'ab',
edu:'av',
degres:{
a1:'',
b1:'1'
},
platform:undefined
}
function iterater(obj){
let blankValues = [];
Object.keys(obj).map((key) => {
if (obj.hasOwnProperty(key) && (typeof obj[key] === "object")) {
blankValues.push(...iterater(obj[key]))
} else {
if (typeof obj[key] === "undefined" || obj[key] === ''){
blankValues.push(key);
}
}
})
return blankValues;
}
console.log(iterater(obj))
You must push the result returned by the recursive call to your array.
Change:
iterater(obj[key])
for:
blankValues.push(...iterater(obj[key]))
const obj = {
name: 'ab',
edu: 'av',
degres: {
a1: '',
b1: '1'
},
platform: undefined
}
function iterater(obj){
let blankValues = [];
Object.keys(obj).map((key) => {
if (obj.hasOwnProperty(key) && (typeof obj[key] === "object")) {
blankValues.push(...iterater(obj[key]))
} else {
if (typeof obj[key] === "undefined" || obj[key] === ''){
blankValues.push(key);
}
}
})
return blankValues;
}
console.log(iterater(obj));
Here is another way to do it using Object.entries(), Object.keys(), Array.reduce(), Array.flat() and Array.isArray(). This implementation works for arrays too.
const obj = {
name:'ab',
edu:'av',
something: [{ a: 1 }, { a: '' }],
degres:{
a1:'',
b1:'1'
},
platform:undefined
};
function getEmptyProps(obj) {
if (!Object.keys(obj).length) { return []; }
return Object.entries(obj).reduce((acc, [key, val]) => {
if (val === undefined || val === null || val.toString().trim() === '') {
acc.push(key);
} else if (Array.isArray(val)) {
acc.push(val.map(getEmptyProps).flat());
} else if (typeof val === 'object') {
acc.push(getEmptyProps(val));
}
return acc.flat();
}, []);
}
console.log(getEmptyProps(obj))
You could take a check for falsy keys and return the key, if the property is an object, the check the object.
const
getFalsy = o => Object.keys(o).reduce((r, k) => {
if (!o[k]) return [...r, k];
if (typeof o[k] === 'object') return [...r, ...getFalsy(o[k])];
return r;
}, []),
object = { name: 'ab', edu: 'av', degres: { a1: '', b1: '1' }, platform: undefined };
console.log(getFalsy(object));