TransWikia.com

sort object properties and JSON.stringify

Stack Overflow Asked by Innovine on December 3, 2021

My application has a large array of objects, which I stringify and save them to the disk. Unfortunately, when the objects in the array are manipulated, and sometimes replaced, the properties on the objects are listed in different orders (their creation order?). When I do JSON.stringify() on the array and save it, a diff shows the properties getting listed in different orders, which is annoying when trying to merge the data further with diff and merging tools.

Ideally I would like to sort the properties of the objects in alphabetical order prior to performing the stringify, or as part of the stringify operation. There is code for manipulating the array objects in many places, and altering these to always create properties in an explicit order would be difficult.

Suggestions would be most welcome!

A condensed example:

obj = {}; obj.name="X"; obj.os="linux";
JSON.stringify(obj);
obj = {}; obj.os="linux"; obj.name="X";
JSON.stringify(obj);

The output of these two stringify calls are different, and showing up in a diff of my data, but my application doesn’t care about the ordering of properties. The objects are constructed in many ways and places.

24 Answers

I just rewrote one of mentioned examples to use it in stringify

const stringifySort = (key, value) => {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
    return Object.keys(value).sort().reduce((obj, key) => (obj[key]=value[key], obj), {});
};

JSON.stringify({name:"X", os:"linux"}, stringifySort);

Answered by lexa-b on December 3, 2021

Don't be confused with the object monitoring of Chrome debugger. It shows sorted keys in object, even though actually it is not sorted. You have to sort the object before you stringify it.

Answered by user1276700 on December 3, 2021

Here is a clone approach...clone the object before converting to json:

function sort(o: any): any {
    if (null === o) return o;
    if (undefined === o) return o;
    if (typeof o !== "object") return o;
    if (Array.isArray(o)) {
        return o.map((item) => sort(item));
    }
    const keys = Object.keys(o).sort();
    const result = <any>{};
    keys.forEach((k) => (result[k] = sort(o[k])));
    return result;
}

If is very new but seems to work on package.json files fine.

Answered by Corey Alix on December 3, 2021

After all, it needs an Array that caches all keys in the nested object (otherwise it will omit the uncached keys.) The oldest answer is just plain wrong, because second argument doesn't care about dot-notation. So, the answer (using Set) becomes.

function stableStringify (obj) {
  const keys = new Set()
  const getAndSortKeys = (a) => {
    if (a) {
      if (typeof a === 'object' && a.toString() === '[object Object]') {
        Object.keys(a).map((k) => {
          keys.add(k)
          getAndSortKeys(a[k])
        })
      } else if (Array.isArray(a)) {
        a.map((el) => getAndSortKeys(el))
      }
    }
  }
  getAndSortKeys(obj)
  return JSON.stringify(obj, Array.from(keys).sort())
}

Answered by Polv on December 3, 2021

The accepted answer does not work for me for nested objects for some reason. This led me to code up my own. As it's late 2019 when I write this, there are a few more options available within the language.

Update: I believe David Furlong's answer is a preferable approach to my earlier attempt, and I have riffed off that. Mine relies on support for Object.entries(...), so no Internet Explorer support.

function normalize(sortingFunction) {
  return function(key, value) {
    if (typeof value === 'object' && !Array.isArray(value)) {
      return Object
        .entries(value)
        .sort(sortingFunction || undefined)
        .reduce((acc, entry) => {
          acc[entry[0]] = entry[1];
          return acc;
        }, {});
    }
    return value;
  }
}

JSON.stringify(obj, normalize(), 2);

--

KEEPING THIS OLDER VERSION FOR HISTORICAL REFERENCE

I found that a simple, flat array of all keys in the object will work. In almost all browsers (not Edge or Internet explorer, predictably) and Node 12+ there is a fairly short solution now that Array.prototype.flatMap(...) is available. (The lodash equivalent would work too.) I have only tested in Safari, Chrome, and Firefox, but I see no reason why it wouldn't work anywhere else that supports flatMap and standard JSON.stringify(...).

function flattenEntries([key, value]) {
  return (typeof value !== 'object')
    ? [ [ key, value ] ]
    : [ [ key, value ], ...Object.entries(value).flatMap(flattenEntries) ];
}

function sortedStringify(obj, sorter, indent = 2) {
  const allEntries = Object.entries(obj).flatMap(flattenEntries);
  const sorted = allEntries.sort(sorter || undefined).map(entry => entry[0]);
  return JSON.stringify(obj, sorted, indent);
}

With this, you can stringify with no 3rd-party dependencies and even pass in your own sort algorithm that sorts on the key-value entry pairs, so you can sort by key, payload, or a combination of the two. Works for nested objects, arrays, and any mixture of plain old data types.

const obj = {
  "c": {
    "z": 4,
    "x": 3,
    "y": [
      2048,
      1999,
      {
        "x": false,
        "g": "help",
        "f": 5
      }
    ]
  },
  "a": 2,
  "b": 1
};

console.log(sortedStringify(obj, null, 2));

Prints:

{
  "a": 2,
  "b": 1,
  "c": {
    "x": 3,
    "y": [
      2048,
      1999,
      {
        "f": 5,
        "g": "help",
        "x": false
      }
    ],
    "z": 4
  }
}

If you must have compatibility with older JavaScript engines, you could use these slightly more verbose versions that emulate flatMap behavior. Client must support at least ES5, so no Internet Explorer 8 or below.

These will return the same result as above.

function flattenEntries([key, value]) {
  if (typeof value !== 'object') {
    return [ [ key, value ] ];
  }
  const nestedEntries = Object
    .entries(value)
    .map(flattenEntries)
    .reduce((acc, arr) => acc.concat(arr), []);
  nestedEntries.unshift([ key, value ]);
  return nestedEntries;
}

function sortedStringify(obj, sorter, indent = 2) {
  const sortedKeys = Object
    .entries(obj)
    .map(flattenEntries)
    .reduce((acc, arr) => acc.concat(arr), [])
    .sort(sorter || undefined)
    .map(entry => entry[0]);
  return JSON.stringify(obj, sortedKeys, indent);
}

Answered by Miles Elam on December 3, 2021

Surprised nobody has mentioned lodash's isEqual function.

Performs a deep comparison between two values to determine if they are equivalent.

Note: This method supports comparing arrays, array buffers, booleans, date objects, error objects, maps, numbers, Object objects, regexes, sets, strings, symbols, and typed arrays. Object objects are compared by their own, not inherited, enumerable properties. Functions and DOM nodes are compared by strict equality, i.e. ===.

https://lodash.com/docs/4.17.11#isEqual

With the original problem - keys being inconsistently ordered - it's a great solution - and of course it will just stop if it finds a conflict instead of blindly serializing the whole object.

To avoid importing the whole library you do this:

import { isEqual } from "lodash-es";

Bonus example: You can also use this with RxJS with this custom operator

export const distinctUntilEqualChanged = <T>(): MonoTypeOperatorFunction<T> => 
                                                pipe(distinctUntilChanged(isEqual));

Answered by Simon_Weaver on December 3, 2021

I don't understand why the complexity of the current best answers is needed, to get all the keys recursively. Unless perfect performance is needed, it seems to me that we can just call JSON.stringify() twice, the first time to get all the keys, and the second time, to really do the job. That way, all the recursion complexity is handled by stringify, and we know that it knows its stuff, and how to handle each object type :

function JSONstringifyOrder( obj, space )
{
    var allKeys = [];
    var seen = {};
    JSON.stringify(obj, function (key, value) {
        if (!(key in seen)) {
            allKeys.push(key);
            seen[key] = null;
        }
        return value;
    });
    allKeys.sort();
    return JSON.stringify(obj, allKeys, space);
}

Answered by Jor on December 3, 2021

Extending AJP's answer, to handle arrays:

function sort(myObj) {
    var sortedObj = {};
    Object.keys(myObj).sort().forEach(key => {
        sortedObj[key] = _.isPlainObject(myObj[key]) ? sort(myObj[key]) : _.isArray(myObj[key])? myObj[key].map(sort) : myObj[key]
    })
    return sortedObj;
}

Answered by gblff on December 3, 2021

I took the answer from @Jason Parham and made some improvements

function sortObject(obj, arraySorter) {
    if(typeof obj !== 'object')
        return obj
    if (Array.isArray(obj)) {
        if (arraySorter) {
            obj.sort(arraySorter);
        }
        for (var i = 0; i < obj.length; i++) {
            obj[i] = sortObject(obj[i], arraySorter);
        }
        return obj;
    }
    var temp = {};
    var keys = [];
    for(var key in obj)
        keys.push(key);
    keys.sort();
    for(var index in keys)
        temp[keys[index]] = sortObject(obj[keys[index]], arraySorter);       
    return temp;
}

This fixes the issue of arrays being converted to objects, and it also allows you to define how to sort arrays.

Example:

var data = { content: [{id: 3}, {id: 1}, {id: 2}] };
sortObject(data, (i1, i2) => i1.id - i2.id)

output:

{content:[{id:1},{id:2},{id:3}]}

Answered by Peter on December 3, 2021

function FlatternInSort( obj ) {
    if( typeof obj === 'object' )
    {
        if( obj.constructor === Object )
        {       //here use underscore.js
            let PaireStr = _( obj ).chain().pairs().sortBy( p => p[0] ).map( p => p.map( FlatternInSort ).join( ':' )).value().join( ',' );
            return '{' + PaireStr + '}';
        }
        return '[' + obj.map( FlatternInSort ).join( ',' ) + ']';
    }
    return JSON.stringify( obj );
}

// example as below. in each layer, for objects like {}, flattened in key sort. for arrays, numbers or strings, flattened like/with JSON.stringify.

FlatternInSort( { c:9, b: { y: 4, z: 2, e: 9 }, F:4, a:[{j:8, h:3},{a:3,b:7}] } )

"{"F":4,"a":[{"h":3,"j":8},{"a":3,"b":7}],"b":{"e":9,"y":4,"z":2},"c":9}"

Answered by saintthor on December 3, 2021

Works with lodash, nested objects, any value of object attribute:

function sort(myObj) {
  var sortedObj = {};
  Object.keys(myObj).sort().forEach(key => {
    sortedObj[key] = _.isPlainObject(myObj[key]) ? sort(myObj[key]) : myObj[key]
  })
  return sortedObj;
}
JSON.stringify(sort(yourObj), null, 2)

It relies on Chrome's and Node's behaviour that the first key assigned to an object is outputted first by JSON.stringify.

Answered by AJP on December 3, 2021

If objects in the list does not have same properties, generate a combined master object before stringify:

let arr=[ <object1>, <object2>, ... ]
let o = {}
for ( let i = 0; i < arr.length; i++ ) {
  Object.assign( o, arr[i] );
}
JSON.stringify( arr, Object.keys( o ).sort() );

Answered by Niels Gjeding Olsen on December 3, 2021

https://gist.github.com/davidfurlong/463a83a33b70a3b6618e97ec9679e490

const replacer = (key, value) =>
    value instanceof Object && !(value instanceof Array) ? 
        Object.keys(value)
        .sort()
        .reduce((sorted, key) => {
            sorted[key] = value[key];
            return sorted 
        }, {}) :
        value;

Answered by David Furlong on December 3, 2021

You can pass a sorted array of the property names as the second argument of JSON.stringify():

JSON.stringify(obj, Object.keys(obj).sort())

Answered by Christian d'Heureuse on December 3, 2021

You can sort object by property name in EcmaScript 2015

function sortObjectByPropertyName(obj) {
    return Object.keys(obj).sort().reduce((c, d) => (c[d] = obj[d], c), {});
}

Answered by Mayki Nayki on December 3, 2021

Update 2018-7-24:

This version sorts nested objects and supports array as well:

function sortObjByKey(value) {
  return (typeof value === 'object') ?
    (Array.isArray(value) ?
      value.map(sortObjByKey) :
      Object.keys(value).sort().reduce(
        (o, key) => {
          const v = value[key];
          o[key] = sortObjByKey(v);
          return o;
        }, {})
    ) :
    value;
}


function orderedJsonStringify(obj) {
  return JSON.stringify(sortObjByKey(obj));
}

Test case:

  describe('orderedJsonStringify', () => {
    it('make properties in order', () => {
      const obj = {
        name: 'foo',
        arr: [
          { x: 1, y: 2 },
          { y: 4, x: 3 },
        ],
        value: { y: 2, x: 1, },
      };
      expect(orderedJsonStringify(obj))
        .to.equal('{"arr":[{"x":1,"y":2},{"x":3,"y":4}],"name":"foo","value":{"x":1,"y":2}}');
    });

    it('support array', () => {
      const obj = [
        { x: 1, y: 2 },
        { y: 4, x: 3 },
      ];
      expect(orderedJsonStringify(obj))
        .to.equal('[{"x":1,"y":2},{"x":3,"y":4}]');
    });

  });

Deprecated answer:

A concise version in ES2016. Credit to @codename , from https://stackoverflow.com/a/29622653/94148

function orderedJsonStringify(o) {
  return JSON.stringify(Object.keys(o).sort().reduce((r, k) => (r[k] = o[k], r), {}));
}

Answered by aleung on December 3, 2021

A recursive and simplified answer:

function sortObject(obj) {
    if(typeof obj !== 'object')
        return obj
    var temp = {};
    var keys = [];
    for(var key in obj)
        keys.push(key);
    keys.sort();
    for(var index in keys)
        temp[keys[index]] = sortObject(obj[keys[index]]);       
    return temp;
}

var str = JSON.stringify(sortObject(obj), undefined, 4);

Answered by Jason Parham on December 3, 2021

I think that if you are in control of the JSON generation (and it sounds like you are), then for your purposes this might be a good solution: json-stable-stringify

From the project website:

deterministic JSON.stringify() with custom sorting to get deterministic hashes from stringified results

If the JSON produced is deterministic you should be able to easily diff/merge it.

Answered by Stijn de Witt on December 3, 2021

This is same as Satpal Singh's answer

function stringifyJSON(obj){
    keys = [];
    if(obj){
        for(var key in obj){
            keys.push(key);
        }
    }
    keys.sort();
    var tObj = {};
    var key;
    for(var index in keys){
        key = keys[index];
        tObj[ key ] = obj[ key ];
    }
    return JSON.stringify(tObj);
}

obj1 = {}; obj1.os="linux"; obj1.name="X";
stringifyJSON(obj1); //returns "{"name":"X","os":"linux"}"

obj2 = {}; obj2.name="X"; obj2.os="linux";
stringifyJSON(obj2); //returns "{"name":"X","os":"linux"}"

Answered by Giridhar C R on December 3, 2021

I made a function to sort object, and with callback .. which actually create a new object

function sortObj( obj , callback ) {

    var r = [] ;

    for ( var i in obj ){
        if ( obj.hasOwnProperty( i ) ) {
             r.push( { key: i , value : obj[i] } );
        }
    }

    return r.sort( callback ).reduce( function( obj , n ){
        obj[ n.key ] = n.value ;
        return obj;
    },{});
}

and call it with object .

var obj = {
    name : "anu",
    os : "windows",
    value : 'msio',
};

var result = sortObj( obj , function( a, b ){
    return a.key < b.key  ;    
});

JSON.stringify( result )

which prints {"value":"msio","os":"windows","name":"anu"} , and for sorting with value .

var result = sortObj( obj , function( a, b ){
    return a.value < b.value  ;    
});

JSON.stringify( result )

which prints {"os":"windows","value":"msio","name":"anu"}

Answered by rab on December 3, 2021

You can add a custom toJSON function to your object which you can use to customise the output. Inside the function, adding current properties to a new object in a specific order should preserve that order when stringified.

See here:

https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify

There's no in-built method for controlling ordering because JSON data is meant to be accessed by keys.

Here's a jsfiddle with a small example:

http://jsfiddle.net/Eq2Yw/

Try commenting out the toJSON function - the order of the properties is reversed. Please be aware that this may be browser-specific, i.e. ordering is not officially supported in the specification. It works in the current version of Firefox, but if you want a 100% robust solution, you may have to write your own stringifier function.

Edit:

Also see this SO question regarding stringify's non-deterministic output, especially Daff's details about browser differences:

How to deterministically verify that a JSON object hasn't been modified?

Answered by Dave R. on December 3, 2021

Try:

function obj(){
  this.name = '';
  this.os = '';
}

a = new obj();
a.name = 'X',
a.os = 'linux';
JSON.stringify(a);
b = new obj();
b.os = 'linux';
b.name = 'X',
JSON.stringify(b);

Answered by 3y3 on December 3, 2021

The simpler, modern and currently browser supported approach is simply this:

JSON.stringify(sortMyObj, Object.keys(sortMyObj).sort());

However, this method does remove any nested objects that aren't referenced and does not apply to objects within arrays. You will want to flatten the sorting object as well if you want something like this output:

{"a":{"h":4,"z":3},"b":2,"c":1}

You can do that with this:

var flattenObject = function(ob) {
    var toReturn = {};
    
    for (var i in ob) {
        if (!ob.hasOwnProperty(i)) continue;
        
        if ((typeof ob[i]) == 'object') {
            var flatObject = flattenObject(ob[i]);
            for (var x in flatObject) {
                if (!flatObject.hasOwnProperty(x)) continue;
                
                toReturn[i + '.' + x] = flatObject[x];
            }
        } else {
            toReturn[i] = ob[i];
        }
    }
    return toReturn;
};
var myFlattenedObj = flattenObject(sortMyObj);
JSON.stringify(myFlattenedObj, Object.keys(myFlattenedObj).sort());

To do it programmatically with something you can tweak yourself, you need to push the object property names into an array, then sort the array alphabetically and iterate through that array (which will be in the right order) and select each value from the object in that order. "hasOwnProperty" is checked also so you definitely have only the object's own properties. Here's an example:

var obj = {"a":1,"b":2,"c":3};

function iterateObjectAlphabetically(obj, callback) {
    var arr = [],
        i;
    
    for (i in obj) {
        if (obj.hasOwnProperty(i)) {
            arr.push(i);
        }
    }

    arr.sort();
    
    for (i = 0; i < arr.length; i++) {
        var key = obj[arr[i]];
        //console.log( obj[arr[i]] ); //here is the sorted value
        //do what you want with the object property
        if (callback) {
            // callback returns arguments for value, key and original object
            callback(obj[arr[i]], arr[i], obj);
        }
    }
}

iterateObjectAlphabetically(obj, function(val, key, obj) {
    //do something here
});

Again, this should guarantee that you iterate through in alphabetical order.

Finally, taking it further for the simplest way, this library will recursively allow you to sort any JSON you pass into it: https://www.npmjs.com/package/json-stable-stringify

var stringify = require('json-stable-stringify');
var obj = { c: 8, b: [{z:6,y:5,x:4},7], a: 3 };
console.log(stringify(obj));

Output

{"a":3,"b":[{"x":4,"y":5,"z":6},7],"c":8}

Answered by marksyzm on December 3, 2021

There is Array.sort method which can be helpful for you. For example:

yourBigArray.sort(function(a,b){
    //custom sorting mechanism
});

Answered by Egor4eg on December 3, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP