I read lots of information on the net and forums but cannot get a solution to my problem.
I have the following JSON file:
var data =
{ "AA" :[{"a1":[{"ab1": [
{"ab1a": 10 },
{"ab1b": 20 },
{"ab1c": 30 },
{"ab1d": 40 }
]
},
{"ab2":[
{"ab1a": 10 },
{"ab1b": 20 },
{"ab1c": 30 },
{"ab1d": 40 }
]
}
]
}
,{"a2":[{"ab3": [
{"ab3a": 10 },
{"ab3b": 20 },
{"ab3c": 30 },
{"ab3d": 40 }
]
},
{"ab4":[
{"ab4a": 10 },
{"ab4b": 20 },
{"ab4c": 30 },
{"ab4d": 40 }
]
}
]
}
]
}
I have validated the JSON file. I want to get the keys first for "AA" then "a1" and "a2" and then for "ab1", "ab2" and etc. There are questions and information about values but not the keys. Most probably jQuery might help but I am confused about how to get all the keys.
Any ideas? any hope...!
The key to getting all the keys from an object of unknown size is recursion. Here I have 2 solutions; one Functional and one Object Oriented. Also I created some other data to test with in addition to yours.
Here is the repo with the complete code: https://github.com/vasilionjea/json-keys
The Data:
var data = {
continents: {
europe: {
countries: [
{
'country-name': 'italy',
cities: [
{ 'city-name': 'Rome', title: 'Roma Dolor', population:873, gdb: 301 },
{ 'city-name': 'Venice', title: 'Venice Veggies.', population:456, gdb: 244 }
]
},
{
'country-name': 'france',
cities: [
{ 'city-name': 'Paris', title: 'De Ipsum', population:7123, gdb: 77 },
{ 'city-name': 'Marseille', title: 'La Mipsum', population:73, gdb: 7 }
]
}
]
},
'north-america': {
countries: [
{
'country-name': 'canada',
cities: [
{ 'city-name': 'Montreal', title: 'The city of lorem ipsum!', population:99, gdb: 011 },
{ 'city-name': 'Quebec', title: 'Veggie Ipsum.', population:123, gdb: 101 }
]
},
{
'country-name': 'usa',
cities: [
{ 'city-name': 'New York', title: 'Empire State', population:1001001, gdb: 1010 },
{ 'city-name': 'San Francisco', title: 'SF Bridge', population:20123, gdb: 202 }
]
}
]
}
}
}
The functional way:
/*
* Functional example.
* Retrieves all keys from an object of unknown size using recursion.
*/
function _isObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
function _eachKey(obj, callback) {
Object.keys(obj).forEach(callback);
}
var _container = [];
function collectKeys(data) {
_eachKey(data, function(key) {
_container.push(key);
if (_isObject(data[key])) {
collectKeys(data[key]);
}
if (Array.isArray(data[key])) {
// Because we're looping through an array and each array's item is an object,
// the `collectKeys` function is receiving each object of the array as an argument.
data[key].forEach(collectKeys);
}
});
return _container;
}
// Execute the function
var myKeys = collectKeys(data);
The Object Oriented way:
/*
* Object Oriented example.
* Retrieves all keys from an object of unknown size using recursion.
*/
function JSONKeys(obj) {
this._container = [];
if (this._isObject(obj)) {
// Just a normal object literal.
this._data = obj;
} else if (typeof obj === 'string') {
// Just in case the data was passed in as a valid JSON string.
this._data = JSON.parse(obj);
} else {
throw new Error('The provided argument must be an object literal or a JSON string.');
}
}
JSONKeys.prototype = {
constructor: JSONKeys,
_isObject: function(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
},
_eachKey: function(obj, callback) {
// Using `bind(this)` makes sure that the `this` value in the `_collect` method
// isn't set to the global object (window).
Object.keys(obj).forEach(callback.bind(this));
},
_recur: function(key, data) {
// If this key's value is also an object go deeper.
if (this._isObject(data[key])) {
this._collect(data[key]);
}
if (Array.isArray(data[key])) {
// * Because we're looping through an array and each array's item is an object,
// the `_collect` method is receiving each object of the array as an argument.
// * Using `bind(this)` makes sure that the `this` value in the `_collect` method
// isn't set to the global object (window).
data[key].forEach(this._collect.bind(this));
}
},
_collect: function(data) {
this._eachKey(data, function(key) {
this._container.push(key);
// Continue collecting
this._recur(key, data);
});
},
getAll: function() {
this._collect(this._data);
return this._container;
}
};
// Create a new instance passing the data.
var keysObject = new JSONKeys(data);
allKeys = keysObject.getAll();
Related
I already found out, how to get the last elements of a deep nested object.
See here for working example: How to get the last children in a deeply nested array with objects
Now I dont want to log the names like in the example with console.log(subObj.name), instead I want to save them in an array, which shouldnt be global. I just want a function return this array.
Is this somehow possible without declaring a global array for this ?
This is my code:
function childrenNames (obj) {
var lastChildren = [];
obj.forEach((subObj) => {
if (subObj.hasOwnProperty('specification') && subObj.specification instanceof Array && subObj.specification.length > 0) {
childrenNames(subObj.specification);
} else {
if (subObj.hasOwnProperty('name')) {
lastChildren.push(subObj.name)
}
}
})
console.log(lastChildren);
return lastChildren
}
But its just returning 4 different arrays instead of 1 containing all last children.
I am not sure whether this is a valid answer and I do not understand it but I am leaving it for now because it does appear, superficially at least, to answer the question. It does not require a global array to be declared as far as I can tell?
var obj = [
{
name: 'something',
children: [
{
name: 'something',
children: [
{
name: 'no child'
},
{
name: 'something empty',
children: [ ]
}
]
}
]
},
{
name: 'something',
children: [
{
name: 'something',
children: [
{
name: 'no child'
}
]
}
]
},
{
name: "children isn't an array",
children: 42
}
]
function childrenNames (obj) {
var lastChildren = [];
obj.forEach((subObj) => {
if (subObj.hasOwnProperty('specification') && subObj.specification instanceof Array && subObj.specification.length > 0) {
childrenNames(subObj.specification);
} else {
if (subObj.hasOwnProperty('name')) {
lastChildren.push(subObj.name)
}
}
})
// console.log(lastChildren);
return lastChildren
}
const res = childrenNames(obj);
console.log('res', res);
So, basically, I have this object (in vue data()):
...
detail: {
title: 'I have no idea',
date: 2020-02-12,
sender: {
name: 'John Cenna',
username: 'john10'
},
receipent: {
name: 'Katleen',
username: 'katmeow'
},
....
}
Now, I want to assign all of them to be null (for validation process, kind of) but couldn't assign the nested object (the object without child is fine with just loop). How do I do this? thank you
A recursive solution:
Use Object.entries() to iterate the key-value entries of the object as follows. Note that Object.entries() returns an empty array for non-objects.
If the entry's value is already null or undefined, return.
If the entry's value is an array, recurse on each array item (go to step 1 with the array item as the new object to evaluate).
If the entry's value is an object, recurse on the object.
Otherwise, set the entry's value to null.
const data = {
detail: {
title: 'I have no idea',
date: 2020 - 02 - 12,
sender: {
name: 'John Cenna',
username: 'john10'
},
receipent: {
name: 'Katleen',
username: 'katmeow'
},
items: [
{ id: 100, name: 'Apple' },
{ id: 200, name: 'Banana' },
{ id: 300, name: 'Orange' },
]
}
}
function nullify(obj) {
// 1
Object.entries(obj).forEach(([k,v]) => {
// 2
if (v === null || v === undefined) {
return
}
// 3
if (Array.isArray(v)) {
return v.forEach(nullify)
}
// 4
if (typeof v === 'object') {
nullify(obj[k])
} else {
// 5
obj[k] = null
}
})
}
nullify(data.detail)
console.log('res', data.detail)
Sorry for the title, It's limited to 150 characters.
Full code example:
https://jsfiddle.net/c81zw30m/
Data:
Let's say I make an API request and I get this JSON object returned:
[
{
id: 123,
person: {
data: {
name: 'John',
language: 'Javascript'
}
},
details: {
age: 25
},
has_experience: true
},
{
id: 456,
person: {
data: {
name: 'Peter',
language: null // here we have null as a value.
}
},
details: {
age: 40
},
has_experience: false
},
{
id: 789,
person: {
data: {
name: 'Paul',
language: 'Python'
}
},
details: {
age: 30
},
has_experience: null // and here we also don't know if the person is available
},
];
Goal:
The end goal here is to iterate over the array and end up with new array of objects with different key names. Say for example I want to replace the key of person with human or the key of available with availability.
Additionally (optionally) we want to skip adding keys which value is equal to null.
Current solution:
let results = [];
for (let i=0; i< json.length; i++) {
results.push({
user_id: json[i].id,
name: json[i].person.data.name,
age: json[i].details.age,
has_experience: json[i].available ? json[i].available : false // here we are assigning a value no matter what using a ternary operator, what if we want no key:value pair here, just skip that pair
});
if (json[i].person.data.language) { results[i].language = json[i].person.data.language }
}
console.log(results);
Problem:
Now the example and solution I provided works, but imagine if the original API request had hundreds of key:value pairs, and many of them might be of null value.
Question:
Using modern javascript, is there any less verbose and more clean looking/elegant way to handle this problem?
Overall I am looking to create a brand new array of objects based on the original one, but with new key names where necessary. Additionally, we want to skip adding some of them if the value of the key is null for example.
Cheers.
EDIT:
Changed the key name from the example originally provided from available to has_experience because it was a bit misleading. I am not looking to filter out the original array of objects based on the value of a given key. If I wanted to do that I'd start with filter and then chain on.
What I want to do is to omit adding a key:value pair in the newly formed array if the value of the key is null for example.
Using lodash (or similar), you could get your mapping definition out of the mapping loop.
I find the following reasonably concise, though it can probably be shortened a little further.
import { get, set } from "lodash";
let json = [ ... ];
let mapping = new Map([
["user_id", "id"],
["name", "person.data.name"],
["age", "details.age"],
["availability", "available"],
["language", "person.data.language"],
["some.nested.property", "person.data.language"]
]);
var results = json.map(element => {
var mappedElement = {};
mapping.forEach((path, field, map) => {
var value = get(element, path);
if (value) {
set(mappedElement, field, value);
}
});
return mappedElement;
});
console.log(results);
Running this on your data yields
[Object, Object, Object]
0: Object
user_id: 123
name: "John"
age: 25
availability: true
language: "Javascript"
some: Object
nested: Object
property: "Javascript"
1: Object
user_id: 456
name: "Peter"
age: 40
2: Object
user_id: 789
name: "Paul"
age: 30
language: "Python"
some: Object
nested: Object
property: "Python"
Working example: https://codesandbox.io/s/mnkp79668
You can try something like this
You can achieve with map()
let json = [{id: 123, person: { data: { name: 'John', language: 'Javascript' } }, details: { age: 25 }, has_experience: true },
{id: 456, person: { data: { name: 'Peter',language: null } }, details: { age: 40 }, has_experience: false},
{id: 789, person: { data: { name: 'Paul', language: 'Python' } }, details: { age: 30 }, has_experience: null },];
let results = [];
results = json.map(current => {
let temp = {
user_id: current.id,
name: current.person.data.name,
age: current.details.age,
}
if (current.has_experience) {
temp.availablity = current.has_experience
}
if (current.person.data.language)
{ temp.language = current.person.data.language }
return temp;
})
console.log(results);
You have two separate problems to resolve. The first appears to be a requirement for generic flattening of the nested data structures within the input with out specifying every possible key that might exist.
This function will recursively flatten a nested object, along the way omitting any null values. However, this function might overwrite any values where the same key exists at multiple levels, so see below.
function flatten(obj, dest) {
for (let key in obj) {
if (typeof obj[key] === 'object') {
flatten(obj[key], dest);
} else if (obj[key] !== null) {
dest[key] = obj[key];
}
}
return dest;
}
You also want to re-map some of the keys in your data, where the below function can be used both as a pre-processor to convert known duplicate keys into unique keys, and can also be used as a post-processor to convert particular keys back into nested objects. NB: requires "lodash".
function remap(obj, keys) {
for (let [in_key, out_key] of keys) {
let val = _.get(obj, in_key, null);
if (val !== null) {
_.unset(obj, in_key);
_.set(obj, out_key, val);
}
}
return obj;
}
The functions can be chained together like this:
let in_map = new Map([
['user.id', 'user_id']
]);
let out_map = new Map([
['available', 'test.availability']
]);
let out = data.map(obj => remap(obj, in_map))
.map(obj => flatten(obj, {}))
.map(obj => remap(obj, out_map));
I think what you want is to first filter the list, then map over the filtered results to create the new structure. This may not be especially performant however if the list is quite large.
const list = [
{
id: 123,
person: {
data: {
name: 'John',
language: 'Javascript'
}
},
details: {
age: 25
},
available: true
},
{
id: 456,
person: {
data: {
name: 'Peter',
language: null // here we have null as a value.
}
},
details: {
age: 40
},
available: false
},
{
id: 789,
person: {
data: {
name: 'Paul',
language: 'Python'
}
},
details: {
age: 30
},
available: null // and here we also don't know if the person is available
},
];
const newList = list.filter(listItem => listItem.available).map(filteredItem => {
return {
user_id: filteredItem.id,
name: filteredItem.person.data.name,
age: filteredItem.details.age,
availability: !!filteredItem.available
}
})
document.getElementById('list').innerText = JSON.stringify(list, null, 2);
document.getElementById('newList').innerText = JSON.stringify(newList, null, 2);
.container {
display: flex;
}
.container pre {
flex: 0 0 50%;
}
<div class="container">
<pre id="list"></pre>
<pre id="newList"></pre>
</div>
For my current project, I'm working with an API that returns data formatted like this:
{
groups: [
{
items: [
{
points: [
{ name: "name1", ... },
{ name: "name2", ... },
{ name: "name3", ... },
...
],
...
},
...
]
},
...
],
...
};
I'd like to create a pure function, mapValues, that takes in an object in the above format, as well as an object mapping each name to a value, and returns the same structure, but with each point containing the value that corresponds to its name.
For example, calling mapValues(data, { name1: "value1", name2: "value2", name3: "value3" }) should return this:
{
groups: [
{
items: [
{
points: [
{ name: "name1", value: "value1", ... },
{ name: "name2", value: "value2", ... },
{ name: "name3", value: "value3", ... },
...
],
...
},
...
]
},
...
],
...
};
Here's my first pass:
function mapValues(data, values) {
return _.extend({}, data, {
groups: _.map(ui.groups, (group) => {
return _.extend({}, group, {
items: _.map(group.items, (item) => {
return _.extend({}, item, {
points: _.map(item.points, (point) => {
return _.extend({ value: values[point.name] }, point);
})
});
})
});
})
});
}
That works, but there's quite a bit of nesting a duplicate code. For my second attempt, I reached for recursion.
function mapValues(data, values) {
return (function recursiveMap(object, attributes) {
if (attributes.length === 0) { return _.extend({ value: values[object.name] }, object); }
let key = attributes[0];
return _.extend({}, object, {
[key]: _.map(object[key], child => recursiveMap(child, attributes.slice(1)))
});
})(ui, ["groups", "items", "points"]);
}
That works too, but it's difficult to read and not very concise.
Is there a cleaner way to recursively map an object using Lodash? Ideally, I'm looking for a functional approach.
Here's a way you can do it using Object.assign and no fancy functions
var data = <your data here>;
var values = <your mapped values>;
data.groups.items.points.map(p=>
Object.assign(p, {value: values[p.name]})
);
This works because arrays and objects are pass by reference. So any modifications to the values will result in the original being changed.
If you don't want to mutate your original dataset, it requires you to use {} as the first argument (to assign to a new, empty object) and show clear read/write paths for each object.
var newData = Object.assign({}, data,
{groups:
{items:
{points: data.groups.items.points.map(p=>
Object.assign({}, p, {value: values[p.name]})
)}
}
}
);
I know you wanted Lodash, but I had same issue some time ago and I've came up with this JSFiddle I am using great tool created by nervgh. It is very simple yet usefull. You can adjust this function to be pure using Object.assign, accept key parameter and tweak it however you want. You get the idea.
function mapValues(data, values) {
var iterator = new RecursiveIterator(data);
for(let {node} of iterator) {
if(node.hasOwnProperty('name')) {
node.value = values[node.name];
}
}
return data;
}
I get json from client side which is nested, I want to faltten it and make the children object keys, preferably using underscore.js.
For example that is my json:
var data = {
or:[
{
dealershipCompany : 11
},
{
authType: 'google'
}],
and: [
{
or: [
{
firstName: {'contains': 'search'}
},
{
lastName: {'contains': 'search'}
},
{
email: {'contains': 'search'}
}]
}]
};
I want to remove both 'or' & 'and'
and when I get the object keys using Object.keys(data) I get
['0','1','2','3','4','5']
but I want it to be like this
['dealershipCompany', 'authType', 'firstName', 'lastName','email']
I tried several times to flatten it by myself but always object keys get numbered
Here the link of jsFiddle
This should work:
var data = {
or:[
{ dealershipCompany : 11 },
{ authType: 'google' }
],
and: [ {
or: [
{ firstName: {'contains': 'search'} },
{ lastName: {'contains': 'search'} },
{ email: {'contains': 'search'} }
]
}]
};
function getOnlyObjects(data) {
var result = [];
if (Array.isArray(data)) {
data.forEach(function(item) {
result = result.concat(
getOnlyObjects(item)
);
});
}
else {
Object.keys(data).forEach(function(key) {
if (Array.isArray(data[key])) {
result = result.concat(
getOnlyObjects(data[key])
);
}
else {
result = result.concat(data);
}
});
}
return result;
}
function getData(data) {
return getOnlyObjects(data).map(function(item) {
return Object.keys(item)[0];
});
}
console.log(getData(data));
Outputs:
["dealershipCompany", "authType", "firstName", "lastName", "email"]
When you use Object.keys over array, you will get indexes, hence you were getting ['0','1','2','3','4','5'].
Edit 1
Have migrated === 'and', === 'or' to an array exceptionList. You can add further keys that you need to filter. This will keep filtering manageable and condition clean.
Code
JSFiddle
var data = {
or: [{
dealershipCompany: 11
}, {
authType: 'google'
}],
and: [{
or: [{
firstName: {
'contains': 'search'
}
}, {
lastName: {
'contains': 'search'
}
}, {
email: {
'contains': 'search'
}
}, ]
}]
};
var result = [];
// You can add further keys that you want to filter
var exceptionList = ["and", "or"];
function getKeys(obj) {
var _keys = Object.keys(obj);
_keys.forEach(function(key) {
// Check if key is either,`and`, `or`, or an index of array.
if (exceptionList.indexOf(key) >=0 || !isNaN(key)) {
getKeys(obj[key]);
} else {
result.push(key);
}
});
}
getKeys(data);
console.log(result)