26

I have some structured JSON data like so. Let's assume this is interchangeable, via JSON.parse():

[
    {
        "title": "pineapple",
        "uid": "ab982d34c98f"
    },
    {
        "title": "carrots",
        "uid": "6f12e6ba45ec"
    }
]

I need it to look like this, remapping title to name, and uid to id with the result:

[
    {
        "name": "pineapple",
        "id": "ab982d34c98f"
    },
    {
        "name": "carrots",
        "id": "6f12e6ba45ec"
    }
]

The most obvious way of doing it is like this:

str = '[{"title": "pineapple","uid": "ab982d34c98f"},{"title": "carrots", "uid": "6f12e6ba45ec"}]';

var arr = JSON.parse(str);
for (var i = 0; i<arr.length; i++) {
    arr[i].name = arr[i].title;
    arr[i].id = arr[i].uid;
    delete arr[i].title;
    delete arr[i].uid;
}

str = '[{"title": "pineapple","uid": "ab982d34c98f"},{"title": "carrots",      "uid": "6f12e6ba45ec"}]';

var arr = JSON.parse(str);
for (var i = 0; i<arr.length; i++) {
    arr[i].name = arr[i].title;
    arr[i].id = arr[i].uid;
    delete arr[i].title;
    delete arr[i].uid;
}

$('body').append("<pre>"+JSON.stringify(arr, undefined, 4)+"</pre>");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

...or using something more complex (albeit not more efficient) like this.

This is all fine and dandy, but what if there were 200,000 objects in the array? This is a lot of processing overhead.

Is there a more efficient way to remap a key name? Possibly without looping through the entire array of objects? If your method is more efficient, please provide proof/references.

gman
  • 100,619
  • 31
  • 269
  • 393
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • Consider creating an array with `{from, to}` pairs, and then using the [`map`](http://www.thatjsdude.com/jsConcepts/concepts/array.html) function to iterate through the `arr` – lexasss Jan 15 '14 at 21:23
  • FYI, this problem has nothing to do with JSON, unless you want to do string processing to change the keys. – Felix Kling Jan 15 '14 at 21:24
  • how do you expect to modify every object in the array without going through the array? that doesn't make sense. Why do you need to remap? – Brian Glaz Jan 15 '14 at 21:30
  • You *could* compare your way to a regex replace, e.g. `str = str.replace(/"title":/g, '"name":');`. This of course makes certain assumptions on the values the objects can have, but it's easy to do, so why not give that a try? – Felix Kling Jan 15 '14 at 21:33
  • Possible duplicate of [Change key name in nested JSON structure](http://stackoverflow.com/questions/13518762/change-key-name-in-nested-json-structure) – Gabe Apr 14 '16 at 22:33

8 Answers8

31

As I already mentioned in the comments, if you can make certain assumptions about the values of the objects, you could use a regular expression to replace the keys, for example:

str = str.replace(/"title":/g, '"name":');

It's not as "clean", but it might get the job done faster.


If you have to parse the JSON anyway, a more structured approach would be to pass a reviver function to JSON.parse and you might be able to avoid an additional pass over the array. This probably depends on how engine implement JSON.parse though (maybe they parse the whole string first and then make a second pass with the reviver function, in which case you wouldn't get any advantage).

var arr = JSON.parse(str, function(prop, value) {
   switch(prop) {
     case "title":
        this.name = value;
        return;
     case "uid":
        this.id = value;
        return;
     default:
        return value;
   }
});

Benchmarks, using the Node.js script below to test 3 times:

1389822740739: Beginning regex rename test
1389822740761: Regex rename complete
// 22ms, 22ms, 21ms
1389822740762: Beginning parse and remap in for loop test
1389822740831: For loop remap complete
// 69ms, 68ms, 68ms
1389822740831: Beginning reviver function test
1389822740893: Reviver function complete
// 62ms, 61ms, 60ms

It appears as if the regex (in this case) is the most efficient, but be careful when trying to parse JSON with regular expressions.


Test script, loading 100,230 lines of the OP's sample JSON:

fs = require('fs');
fs.readFile('test.json', 'utf8', function (err, data) {
    if (err) {
        return console.log(err);
    }
    console.log(new Date().getTime() + ": Beginning regex rename test");
    var str = data.replace(/"title":/g, '"name":');
    str = str.replace(/"uid":/g, '"id":');
    JSON.parse(str);
    console.log(new Date().getTime() + ": Regex rename complete");
    console.log(new Date().getTime() + ": Beginning parse and remap in for loop test");
    var arr = JSON.parse(data);
    for (var i = 0; i < arr.length; i++) {
        arr[i].name = arr[i].title;
        arr[i].id = arr[i].uid;
        delete arr[i].title;
        delete arr[i].uid;
    }
    console.log(new Date().getTime() + ": For loop remap complete");
    console.log(new Date().getTime() + ": Beginning reviver function test");
    var arr = JSON.parse(data, function (prop, value) {
        switch (prop) {
            case "title":
                this.name = value;
                return;
            case "uid":
                this.id = value;
                return;
            default:
                return value;
        }
    });
    console.log(new Date().getTime() + ": Reviver function complete");
});
Community
  • 1
  • 1
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • Still looks like the regex is the quickest -- I'm only testing this in a simple Node.js instance, I can update your answer with the benchmarks if you like? – brandonscript Jan 15 '14 at 21:52
  • @remus: Yep, that would be cool! Sorry, I don't have the time now to make those tests myself. – Felix Kling Jan 15 '14 at 21:53
  • 1
    OK, updated. Hope you don't mind I hijacked it like that, but there's a wealth of information there now. – brandonscript Jan 15 '14 at 21:58
  • Awesome! Nice to see that the reviver function is at least a little faster (though not by much). And yeah, you have to be careful with regular expressions and be certain about the input you get. – Felix Kling Jan 15 '14 at 22:03
  • Luckily in my case I'm using consistent data with a consistent schema, so it won't be a problem. And also in my case, since I need this to server thousands of requests in Node, I want it to be as efficient as possible. Thanks! – brandonscript Jan 15 '14 at 22:05
  • You're welcome! You could also try to combine both replace options to something like `str.replace(/"(name|uid)":/g, function(match, name) { return keymap[name] || match;})` where `keymap` is something like `{name: '"name":', ....}`. You'd save an additional pass over the string, but at the cost of function calls. – Felix Kling Jan 15 '14 at 22:07
  • Yeah, actually I think that's what I will end up doing. – brandonscript Jan 15 '14 at 22:13
  • `var arr = JSON.parse(data, function (prop, value) { switch (prop) { case "title": this.name = value; return; case "uid": this.id = value; return; default: return value; } });` i am trying to run this code in dev console, but can't. help needed. `data` i am giving as json object `VM995:1 Uncaught SyntaxError: Unexpected token o in JSON at position 1` getting this error – Ankuli Oct 02 '16 at 19:46
  • @Ankuli: seems like you are passing an object to JSON.parse, not a string containing JSON. Don't confuse JavaScript objects with JSON. – Felix Kling Oct 03 '16 at 00:34
  • sorry, my mean is javascript object only. var data = {'title':'testanksuh'}; – Ankuli Oct 03 '16 at 04:39
  • 1
    @Ankuli: Right. As I said, `JSON.parse` expects to be passed a string, not an object. – Felix Kling Oct 03 '16 at 04:42
17

Asked this question a long time ago, and since then, I've grown acustomed to using Array.prototype.map() to get the job done, more for stability and cleanliness of code than performance. While it's certainly not the most performant, it looks great:

var repl = orig.map(function(obj) {
    return {
        name: obj.title,
        id: obj.uid
    }
})

If you need a more flexible (and ES6-compatible function), try:

let replaceKeyInObjectArray = (a, r) => a.map(o => 
    Object.keys(o).map((key) => ({ [r[key] || key] : o[key] })
).reduce((a, b) => Object.assign({}, a, b)))

e.g.

const arr = [{ abc: 1, def: 40, xyz: 50 }, { abc: 1, def: 40, xyz: 50 }, { abc: 1, def: 40, xyz: 50 }]
const replaceMap = { "abc": "yyj" }

replaceKeyInObjectArray(arr, replaceMap)

/*
[
    {
        "yyj": 1,
        "def": 40,
        "xyz": 50
    },
    {
        "yyj": 1,
        "def": 40,
        "xyz": 50
    },
    {
        "yyj": 1,
        "def": 40,
        "xyz": 50
    }
]
*/
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • It's very unusual to use map to instigate side effects like that, you should use forEach for that use case. But given map returns a new array you should just do the transform in the callback function and save that array to repl. – Stefano Apr 17 '16 at 07:10
10

Here's another take on the OP's suggestion to use map() for clarity (not performance).

var newItems = items.map(item => ({
    name: item.title,
    id: item.uid
}));

This uses ES6 arrow functions and the shortcut syntaxes that are possible when there's only one parm passed to the function and only one statement in the body of the function.

Depending on your history with lambda expressions in various languages, this form may or may not resonate with you.

Be careful when returning an object literal in the arrow function shortcut syntax like this. Don't forget the additional parens around the object literal!

ddanger
  • 101
  • 1
  • 4
2

If you want to make it a little more reusable. Maybe this is a decent approach.

function rekey(arr, lookup) {
 for (var i = 0; i < arr.length; i++) {
  var obj = arr[i];
  for (var fromKey in lookup) {
   var toKey = lookup[fromKey];
   var value = obj[fromKey];
   if (value) {
    obj[toKey] = value;
    delete obj[fromKey];
   }
  }
 }
 return arr;
}

var arr = [{ apple: 'bar' }, { apple: 'foo' }];
var converted = rekey(arr, { apple: 'kung' });
console.log(converted);
luwes
  • 854
  • 11
  • 19
  • 1
    your Run code snippet does not work for me. I'm using chrome and mac. – grepit Sep 10 '15 at 03:39
  • Are you sure? It's `console.log`, it gets logged in your browser console in the Developer Tools. On my end it seems to work with Chrome on Mac. – luwes Sep 10 '15 at 08:08
  • I changed `if (value) {` to `if (typeof value !== "undefined") {` as it was skipping some keys. – Karl_S Sep 24 '16 at 11:47
  • (Sorry for double comments - I hit enter and then was not fast enough on my edit) I also added an option to Reverse the keys as I needed to take the data back to the original. function rekey(arr, lookup, reverse) { //Set a default false if not passed if (reverse === undefined) { reverse = false; } for (var i = 0; i < arr.length; i++) { var obj = arr[i]; for (var fromKey in lookup) { var toKey = lookup[fromKey]; var value = obj[fromKey]; if (typeof value !== 'undefined' && !reverse) {... – Karl_S Sep 24 '16 at 11:54
2

Using ES6:

const renameFieldInArrayOfObjects = (arr, oldField, newField) => {
  return arr.map(s => {
    return Object.keys(s).reduce((prev, next) => {
      if(next === oldField) { 
        prev[newField] = s[next]
      } else { 
        prev[next] = s[next] 
      }
      return prev
    }, {})
  })
}

Using ES7:

const renameFieldInArrayOfObjects = (arr, oldField, newField) => {
  return arr.map(s => {
    return Object.keys(s).reduce((prev, next) => {
      return next === oldField
        ? {...prev, [newField]: s[next]}
        : {...prev, [next]: s[next]}
    }, {})
  })
}
ssomnoremac
  • 729
  • 1
  • 8
  • 16
2

You can use an npm package named node-data-transform.

Your data :

const data = [
  {
    title: 'pineapple',
    uid: 'ab982d34c98f',
  },
  {
    title: 'carrots',
    uid: '6f12e6ba45ec',
  },
];

Your mapping :

const map = {
  item: {
    name: 'title',
    id: 'uid',
  },
};

And use the package :

const DataTransform = require("node-json-transform").DataTransform;
const dataTransform = DataTransform(data, map);
const result = dataTransform.transform();
console.log(result);

Result :

[
  {
    name: 'pineapple',
    id: 'ab982d34c98f'
  },
  {
    name: 'carrots',
    id: '6f12e6ba45ec'
  }
]

Maybe it's not the best way for performance, but it's quite elegant.

yabbal
  • 106
  • 1
  • 9
1
var jsonObj = [/*sample array in question*/   ]

Based on different benchmarks discussed below, fastest solution is native for:

var arr = [];
for(var i = 0, len = jsonObj .length; i < len; i++) {
  arr.push( {"name": jsonObj[i].title, "id" : jsonObj[i].uid});
}

I think alternatively without using a frameworks this will be option 2:

var arr = []
jsonObj.forEach(function(item) { arr.push({"name": item.title, "id" : item.uid }); });

There is always debate between using navite and non-navite functions. If I remember correctly lodash argued they were faster than underscore because the use non-native functions for key operations.

However different browsers will produce sometimes very different results. I always looked for the best average.

For benchmarks you can take a look at this:

http://jsperf.com/lo-dash-v1-1-1-vs-underscore-v1-4-4/8

Dalorzo
  • 19,834
  • 7
  • 55
  • 102
  • Why would this be more efficient? Please elaborate. – Felix Kling Jan 15 '14 at 21:23
  • 3
    According to your benchmark, `Array.map` is actually slower than a `for` loop in both Chrome and Safari. So, that doesn't really help. Moreover, I'm ultimately building this in Node.js, and will be dependent on V8. – brandonscript Jan 15 '14 at 21:31
  • FYI, [here](http://jsperf.com/loop-vs-array-map) is another performance test for this particular problem. I think the result is pretty clear. – Tobias Jan 15 '14 at 21:33
  • @remus If your problems is associated with NodeJS then this is another story and that is why I mentioned the average across browsers. – Dalorzo Jan 15 '14 at 21:37
  • Ouch. The "average across browsers" shows that your solution is slower. – Tobias Jan 15 '14 at 21:38
  • @still_learning if you have an application that runs in IE7 and Chrome you have to consider both scenarios, correct? – Dalorzo Jan 15 '14 at 21:42
  • Well, if IE7 supported `Array.map`, we could start talking about this. Most Chrome builds seem to perform faster with loops as well. But I don't wanna discuss about this, you are right, your solution **could** (theoretically) be faster. – Tobias Jan 15 '14 at 21:46
  • Thank you all your valuable comments, I updated the answer based on our discussion. – Dalorzo Jan 15 '14 at 21:53
  • I think the performance advantage of this solution should not be dicussed in terms of the iteration method (which is trivial to change), but focus on the returning of a new object literal instead of modifying the existing objects. – Bergi Jan 15 '14 at 22:00
  • @Bergi: So, it's worse because it uses more memory? Or what is your opinion on this? – Felix Kling Jan 15 '14 at 22:05
  • @FelixKling: I'd speculate that it's better because [afaik] `delete` is not very optimized in V8, while recurring object literals are. But rather than guessing I'd like someone to do a test on this :-) – Bergi Jan 15 '14 at 22:09
  • @Bergi: I see. I always forget about the various operations that are optimized (or not). Thanks! – Felix Kling Jan 15 '14 at 22:12
0
function replaceElem(value, replace, str) {
            while (str.indexOf(value) > -1) {
                str = str.replace(value, replace);
            }
            return str;
        }

call this from main

var value = "tittle";
var replace = "name";
replaceElem(value, replace, str);