41

According to MDN documentation for JSON.stringify:

Properties of non-array objects are not guaranteed to be stringified in any particular order. Do not rely on ordering of properties within the same object within the stringification.

I had hoped to determine if an object changed by caching a stringified version of the object, then comparing it to a subsequently stringified version of the object. That seemed much simpler than recursively iterating through the object and doing comparisons. The problem is that because the JSON.stringify function is not deterministic, I could technically get a different string when I stringify the same object.

What other options do I have? Or do I have to write a nasty compare function to determine object equality?

Braiam
  • 4,345
  • 11
  • 47
  • 69
aw crud
  • 8,511
  • 17
  • 67
  • 112
  • 1
    You could have a look at [this `JSON` implementation](https://github.com/douglascrockford/JSON-js) and modify it so that the keys are sorted. – Felix Kling Jan 19 '12 at 19:23
  • 3
    Perhaps a bit unrelated, but note that a *fully* deterministic stringifying function is not possible: `JSON.stringify({get a(){return Math.random()}});`. – pimvdb Jan 19 '12 at 19:23
  • This may be overkill, but you can use Backbone JS to set a 'change' event to an object and if it changes it will fire off whatever function you want. That is just a tiny piece of the entire library, but it could be what you're looking for. – Seth Jan 19 '12 at 19:25
  • On a side note, what platform are you using? If possible you could make use of V8's proxy implementation which enables you to add a gateway for a lot of operations (such as setting a property). – pimvdb Jan 19 '12 at 19:37
  • You could try the fast approach: string comparison of the whole, first, and fallback to the slower and more reliable but slower recursive comparison if the fast approach failed. – bart Jan 19 '12 at 20:43
  • To solve your use case of comparing serialized objects, I would use [`JSON.parse`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse) to get the original objects back, and then [test for deep equality](http://stackoverflow.com/questions/201183/how-do-you-determine-equality-for-two-javascript-objects). I suspect that the performance is still better than trying to write your own non-native `stringify` function. – Jo Liss Mar 07 '13 at 17:44
  • 1
    (Of course, this doesn't help you if you want to hash the serialized objects or something.) – Jo Liss Mar 07 '13 at 23:46

8 Answers8

11

I am pretty sure this is because of the way different JavaScript engines keep track of object properties internally. Take this for example:

var obj = {
"1" : "test",
"0" : "test 2"
};

for(var key in obj) {
    console.log(key);
}

This will log 1, 0 in e.g. Firefox, but 0, 1 in V8 (Chrome and NodeJS). So if you need to be deterministic, you will probably have to iterate through each key store it in an array, sort the array and then stringify each property separately by looping through that array.

Daff
  • 41,847
  • 9
  • 99
  • 113
10

You may want to try JSON.sortify, a little helper that I wrote.

In contrast to the answers given so far, it

ThomasR
  • 908
  • 9
  • 15
  • There is also [`json-stable-stringify`](https://github.com/substack/json-stable-stringify). It seems to be slightly older and more mature. The key difference relevant to me is that it supports a custom comparison function. In my case I want to sort by value since my objects are frequency tables and I want maximum human usefulness of the JSON files. I haven't actually tried either module yet. – hippietrail Mar 02 '16 at 03:45
  • 2
    @hippietrail I have seen that before, and you can certainly use it. The major artchitectural difference is that JSON.sortify tries to achieve maximal compatibility with JSON.stringify, i.e. you can simply overwrite `JSON.stringify = JSON.sortify` and be sure that nothing breaks. That is not the case with json-stable-stringify. For example, it does not provide the [replacer parameter](http://www.dyn-web.com/tutorials/php-js/json/filter.php), and it crashes on cyclic values etc. – ThomasR Mar 02 '16 at 08:03
  • @ThomasR: Yes I noticed that too once I played with it. The module seemed worthy of mentioning in this thread but not of a whole separate answer. – hippietrail Mar 02 '16 at 12:46
  • @Flimm Version 2 is now AMD and CommonJS compatible. – ThomasR Mar 11 '16 at 07:14
5

Here's an implementation of a deterministic JSON.stringify() that I wrote (uses Underscore.js). It converts (non-array) objects recursively into sorted key-value pairs (as Arrays), then stringifies those. Original coderwall post here.

Stringify:

function stringify(obj) {
  function flatten(obj) {
    if (_.isObject(obj)) {
      return _.sortBy(_.map(
          _.pairs(obj),
          function(p) { return [p[0], flatten(p[1])]; }
        ),
        function(p) { return p[0]; }
      );
    }
    return obj;
  }
  return JSON.stringify(flatten(obj));
}

Parse:

function parse(str) {
  function inflate(obj, pairs) {
     _.each(pairs, function(p) {
      obj[p[0]] = _.isArray(p[1]) ?
        inflate({}, p[1]) :
        p[1];
    });
    return obj;
  }
  return inflate({}, JSON.parse(str));
}
Jimmy Theis
  • 448
  • 3
  • 8
  • 2
    Do beware, though, that using stringify above, `stringify({0:"a"}) == stringify(["a"])`, which is a little defective. – Peter V. Mørch Jul 01 '13 at 18:36
  • Good point, @PeterV.Mørch. I'll add a note to the description that these functions should be used with proper, non-array objects. – Jimmy Theis Jul 03 '13 at 14:42
  • 2
    Be aware, this isn't just arrays at the top level, but any arrays at arbitrary depth. E.g. note that also: `stringify({a: { 0: "a"}}) == stringify({a: ["a" ]})`. So if the data structure uses arrays anywhere, I'll be able to construct a non-array only structure that gives the same `stringify` string. – Peter V. Mørch Jul 04 '13 at 11:33
3

These days I've been playing with the deterministic way to stringify an object and I've written the ordered object stringify to JSON, that solves the above noted dilemma: http://stamat.wordpress.com/javascript-object-ordered-property-stringify/

Also I was playing with custom hash table implementations which is also related to the topic: http://stamat.wordpress.com/javascript-quickly-find-very-large-objects-in-a-large-array/

//SORT WITH STRINGIFICATION

var orderedStringify = function(o, fn) {
    var props = [];
    var res = '{';
    for(var i in o) {
        props.push(i);
    }
    props = props.sort(fn);

    for(var i = 0; i < props.length; i++) {
        var val = o[props[i]];
        var type = types[whatis(val)];
        if(type === 3) {
            val = orderedStringify(val, fn);
        } else if(type === 2) {
            val = arrayStringify(val, fn);
        } else if(type === 1) {
            val = '"'+val+'"';
        }

        if(type !== 4)
            res += '"'+props[i]+'":'+ val+',';
    }

    return res.substring(res, res.lastIndexOf(','))+'}';
};

//orderedStringify for array containing objects
var arrayStringify = function(a, fn) {
    var res = '[';
    for(var i = 0; i < a.length; i++) {
        var val = a[i];
        var type = types[whatis(val)];
        if(type === 3) {
            val = orderedStringify(val, fn);
        } else if(type === 2) {
            val = arrayStringify(val);
        } else if(type === 1) {
            val = '"'+val+'"';
        }

        if(type !== 4)
            res += ''+ val+',';
    }

    return res.substring(res, res.lastIndexOf(','))+']';
}
stamat
  • 1,529
  • 17
  • 23
3

Using Underscore or Lodash:

var sortByKeys = function(obj) {
  if (!_.isObject(obj)) {
    return obj;
  }
  var sorted = {};
  _.each(_.keys(obj).sort(), function(key) {
    sorted[key] = sortByKeys(obj[key]);
  });
  return sorted;
};

var sortedStringify = function() {
    arguments[0] = sortByKeys(arguments[0]);
    return JSON.stringify.apply(this, arguments);
};

Works in latest Chrome and Firefox.

JSFiddle here: http://jsfiddle.net/stchangg/ruC22/2/

stephjang
  • 849
  • 1
  • 8
  • 12
  • 2
    This relies on the fact that most browsers seem to keep the order of keys in a Javascript object in the order that they were inserted. [This is not guaranteed](http://stackoverflow.com/q/5525795/247696) – Flimm Aug 20 '15 at 09:22
  • 1
    Yeah, this is broken. "var sorted = {};" is a meaningless statement. Objects are not ordered which caused this question in the first place. – Hannes Landeholm Oct 14 '15 at 23:49
2

Recently I've had a similar use case. Following code has no dependencies and works for all browsers:

function stringify(obj) {
  var type = Object.prototype.toString.call(obj);

  // IE8 <= 8 does not have array map
  var map = Array.prototype.map || function map(callback) {
    var ret = [];
    for (var i = 0; i < this.length; i++) {
      ret.push(callback(this[i]));
    }
    return ret;
  };

  if (type === '[object Object]') {
    var pairs = [];
    for (var k in obj) {
      if (!obj.hasOwnProperty(k)) continue;
      pairs.push([k, stringify(obj[k])]);
    }
    pairs.sort(function(a, b) { return a[0] < b[0] ? -1 : 1 });
    pairs = map.call(pairs, function(v) { return '"' + v[0] + '":' + v[1] });
    return '{' + pairs + '}';
  }

  if (type === '[object Array]') {
    return '[' + map.call(obj, function(v) { return stringify(v) }) + ']';
  }

  return JSON.stringify(obj);
};

stringify([{b: {z: 5, c: 2, a: {z: 1, b: 2}}, a: 1}, [1, 2, 3]])

'[{"a":1,"b":{"a":{"b":2,"z":1},"c":2,"z":5}},[1,2,3]]'

stringify([{a: 1, b:{z: 5, c: 2, a: {b: 2, z: 1}}}, [1, 2, 3]])

'[{"a":1,"b":{"a":{"b":2,"z":1},"c":2,"z":5}},[1,2,3]]'

nehz
  • 2,064
  • 2
  • 21
  • 36
1

JavaScript keys are intrinsically unordered. You have to write your own Stringifier to make this work, so I did.

Usage:

JSONc14n.stringify(obj)

Source:

var JSONc14n = {
    stringify: function(obj){
        var json_string,
            keys,
            key,
            i;

        switch(this.get_type(obj)){
            case "[object Array]":
                json_string = "[";
                for(i = 0; i < obj.length; i++){
                    json_string += this.stringify(obj[i]);
                    if(i < obj.length - 1) json_string += ",";
                }
                json_string += "]";
                break;
            case "[object Object]":
                json_string = "{";
                keys = Object.keys(obj);
                keys.sort();
                for(i = 0; i < keys.length; i++){
                    json_string += '"' + keys[i] + '":' + this.stringify(obj[keys[i]]);
                    if(i < keys.length - 1) json_string += ",";
                }
                json_string += "}";
                break;
            case "[object Number]":
                json_string = obj.toString();
                break;
            default:
                json_string = '"' + obj.toString().replace(/["\\]/g,
                    function(_this){
                        return function(character){
                            return _this.escape_character.apply(_this, [character]);
                        };
                    }(this)
                ) + '"';
        }
        return json_string;
    },
    get_type: function(thing){
        if(thing===null) return "[object Null]";
        return Object.prototype.toString.call(thing);
    },
    escape_character: function(character){
        return this.escape_characters[character];
    },
    escape_characters: {
        '"': '\\"',
        '\\': '\\\\'
    }
};
holloway
  • 21
  • 1
  • 3
  • Worked for me on an array of objects, thanks! Ordered the properties of all the nested objects exactly as needed. Tried some of the other solutions in the posts above which even after some tweaking weren't working as I needed em to. Know I'm jumping in here late but hey... – Greg Venech Apr 29 '15 at 12:40
  • 1
    This returns wrong values for the boolean values true and false. I added a case for `'[object Boolean]'` which simply says `jsonStr = obj ? 'true' : 'false';` to get the same result as with native `JSON.stringify`. What's nice about the above code is that it is only about 4 times slower than native, the code in package https://github.com/substack/json-stable-stringify is 10 times slower (tested on node.js 7.2.0). – Mörre Nov 25 '16 at 16:39
0

Some things you may want to consider: What does it mean for an object to be different? Are you looking to see if a property on on that object has changed? Who is interested in 'knowing' about these changes? Do you want to know immediately if an objects property has changed?

You could make the properties on that object 'observable' properties and when that property changes you can fire an event and whoever is interested can subscribe to those property changes. That way you know immediately what has changed and you can do whatever you want with that information. Knockout.js use this approach. This way you do not have to resort to 'nasty' object comparisons

mbx-mbx
  • 1,705
  • 11
  • 23