Renaming object keys recursively
A bit late, but I was looking for a nice short implementation that also deals with arrays (the previous answer doesn't), so I decided to post my generic ES6 implementation, as it may help some folks:
function deepMapKeys(originalObject, callback) {
if (typeof originalObject !== 'object') {
return originalObject
}
return Object.keys(originalObject || {}).reduce((newObject, key) => {
const newKey = callback(key)
const originalValue = originalObject[key]
let newValue = originalValue
if (Array.isArray(originalValue)) {
newValue = originalValue.map(item => deepMapKeys(item, callback))
} else if (typeof originalValue === 'object') {
newValue = deepMapKeys(originalValue, callback)
}
return {
...newObject,
[newKey]: newValue,
}
}, {})
}
For the case in question, the call would be:
deepMapKeys(inputObject, key => (keys_long[keys_short.indexOf(key)] || key))
That being said, if you can use npm, there are several packages out there (here's one, another..)
More concise implementation of the recursive mapping function for object keys:
const objectRecursiveKeyMap = (obj, fn) =>
Object.fromEntries(Object.entries(obj).map(([key, value]) => {
const getValue = v =>
(typeof v === 'object' && v !== null) ? objectRecursiveKeyMap(v, fn) : v
return [fn(key), Array.isArray(value)
? value.map(val => getValue(val))
: getValue(value)]
}))
Using example:
objectRecursiveKeyMap(obj, key => key.toUpperCase())
There are a couple of problems there.
One is that you're falling prey to The Horror of Implicit Globals by failing to declare your build
variable in the function.
But the logic has issues as well, here's a minimal reworking:
var keys_short = ["ch","d","u","tz"];
var keys_long = ["children","data","user_id","time_zone"];
function refit_keys(o){
var build, key, destKey, ix, value;
// Only handle non-null objects
if (o === null || typeof o !== "object") {
return o;
}
// Handle array just by handling their contents
if (Array.isArray(o)) {
return o.map(refit_keys);
}
// We have a non-array object
build = {};
for (key in o) {
// Get the destination key
ix = keys_short.indexOf(key);
destKey = ix === -1 ? key : keys_long[ix];
// Get the value
value = o[key];
// If this is an object, recurse
if (typeof value === "object") {
value = refit_keys(value);
}
// Set it on the result using the destination key
build[destKey] = value;
}
return build;
}
Live Example:
"use strict";
var input = {
"id":"1",
"ch":[
{
"id":"3",
"ch":[
],
"d":{
"u":"3",
"tz":"8.00"
}
},
{
"id":"45",
"ch":[
{
"id":"70",
"ch":[
{
"id":"43",
"ch":[
],
"d":{
"u":"43",
"tz":"-7.00"
}
}
],
"d":{
"u":"70",
"tz":"-7.00"
}
}
],
"d":{
"u":"45",
"tz":"-7.00"
}
}
],
"d":{
"u":"1",
"tz":"8.00"
}
};
var keys_short = ["ch","d","u","tz"];
var keys_long = ["children","data","user_id","time_zone"];
function refit_keys(o){
var build, key, destKey, ix, value;
// Only handle non-null objects
if (o === null || typeof o !== "object") {
return o;
}
// Handle array just by handling their contents
if (Array.isArray(o)) {
return o.map(refit_keys);
}
// We have a non-array object
build = {};
for (key in o) {
// Get the destination key
ix = keys_short.indexOf(key);
destKey = ix === -1 ? key : keys_long[ix];
// Get the value
value = o[key];
// If this is an object, recurse
if (typeof value === "object") {
value = refit_keys(value);
}
// Set it on the result using the destination key
build[destKey] = value;
}
return build;
}
console.log(refit_keys(input));
.as-console-wrapper {
max-height: 100% !important;
}
But rather than parallel arrays, I'd suggest using a mapping, via an object or a Map
:
// Object with no prototype to avoid false matches on `toString` and other built-ins
var mapShortToLong = Object.assign(Object.create(null), {
"ch": "children",
"d": "data",
"u": "user_id",
"tz": "time_zone"
});
function refit_keys(o){
var build, key, destKey, value;
// Only handle non-null objects
if (o === null || typeof o !== "object") {
return o;
}
// Handle array just by handling their contents
if (Array.isArray(o)) {
return o.map(refit_keys);
}
build = {};
for (key in o) {
// Get the destination key
destKey = mapShortToLong[key] || key;
// Get the value
value = o[key];
// If this is an object, recurse
if (typeof value === "object") {
value = refit_keys(value);
}
// Set it on the result using the destination key
build[destKey] = value;
}
return build;
}
Live Example:
"use strict";
var input = {
"id":"1",
"ch":[
{
"id":"3",
"ch":[
],
"d":{
"u":"3",
"tz":"8.00"
}
},
{
"id":"45",
"ch":[
{
"id":"70",
"ch":[
{
"id":"43",
"ch":[
],
"d":{
"u":"43",
"tz":"-7.00"
}
}
],
"d":{
"u":"70",
"tz":"-7.00"
}
}
],
"d":{
"u":"45",
"tz":"-7.00"
}
}
],
"d":{
"u":"1",
"tz":"8.00"
}
};
// Object with no prototype to avoid false matches on `toString` and other built-ins
var mapShortToLong = Object.assign(Object.create(null), {
"ch": "children",
"d": "data",
"u": "user_id",
"tz": "time_zone"
});
function refit_keys(o){
var build, key, destKey, value;
// Only handle non-null objects
if (o === null || typeof o !== "object") {
return o;
}
// Handle array just by handling their contents
if (Array.isArray(o)) {
return o.map(refit_keys);
}
build = {};
for (key in o) {
// Get the destination key
destKey = mapShortToLong[key] || key;
// Get the value
value = o[key];
// If this is an object, recurse
if (typeof value === "object") {
value = refit_keys(value);
}
// Set it on the result using the destination key
build[destKey] = value;
}
return build;
}
console.log(refit_keys(input));
.as-console-wrapper {
max-height: 100% !important;
}
Or in modern JavaScript:
// Using a `Map` here to provide a `Map` example, but you can ue an object as
// in the previous ones if you prefer if the keys are strings
const mapShortToLong = new Map([
["ch", "children"],
["d", "data"],
["u", "user_id"],
["tz", "time_zone"],
]);
function refit_keys(o){
// Only handle non-null objects
if (o === null || typeof o !== "object") {
return o;
}
// Handle array just by handling their contents
if (Array.isArray(o)) {
return o.map(refit_keys);
}
const build = {};
for (const key in o) {
// Get the destination key
const destKey = mapShortToLong.get(key) || key;
// Get the value
let value = o[key];
// If this is an object, recurse
if (typeof value === "object") {
value = refit_keys(value);
}
// Set it on the result using the destination key
build[destKey] = value;
}
return build;
}
Live Example:
"use strict";
var input = {
"id":"1",
"ch":[
{
"id":"3",
"ch":[
],
"d":{
"u":"3",
"tz":"8.00"
}
},
{
"id":"45",
"ch":[
{
"id":"70",
"ch":[
{
"id":"43",
"ch":[
],
"d":{
"u":"43",
"tz":"-7.00"
}
}
],
"d":{
"u":"70",
"tz":"-7.00"
}
}
],
"d":{
"u":"45",
"tz":"-7.00"
}
}
],
"d":{
"u":"1",
"tz":"8.00"
}
};
// Using a `Map` here to provide a `Map` example, but you can ue an object as
// in the previous ones if you prefer if the keys are strings
const mapShortToLong = new Map([
["ch", "children"],
["d", "data"],
["u", "user_id"],
["tz", "time_zone"],
]);
function refit_keys(o){
// Only handle non-null objects
if (o === null || typeof o !== "object") {
return o;
}
// Handle array just by handling their contents
if (Array.isArray(o)) {
return o.map(refit_keys);
}
const build = {};
for (const key in o) {
// Get the destination key
const destKey = mapShortToLong.get(key) || key;
// Get the value
let value = o[key];
// If this is an object, recurse
if (typeof value === "object") {
value = refit_keys(value);
}
// Set it on the result using the destination key
build[destKey] = value;
}
return build;
}
console.log(refit_keys(input));
.as-console-wrapper {
max-height: 100% !important;
}
One of the problem could be that the variable build
is actually a global variable. Hence, it contains only the result of the first depth in the recursion.
Adding a var
before build
should solve part of the problem.