26 Oct 2019 - by 'Maurits van der Schee'
When you are creating unit tests for things that produce JSON, you will quickly find that you need to compare two JSON strings for equality. The order of key/value pairs within a JSON object is not defined, while the order within JSON arrays is. The spec (RFC 7159) says:
An object is an unordered collection of zero or more name/value pairs... An array is an ordered sequence of zero or more values.
In this blog post I'll show how to sort JSON trees in lexicographical order. When the values originate from a set (such as unordered SQL results), then you may want to sort the arrays as well. Turning the sorting of objects and arrays on and off is possible.
Let me show what the code does. The JSON string:
{"id":10,"usr":{"uid":10,"gid":5,"roles":[3,1,2]},"cat":2}
is translated into:
{"cat":2,"id":10,"usr":{"gid":5,"roles":[3,1,2],"uid":10}}
with the sort objects option set to true, or into:
{"cat":2,"id":10,"usr":{"gid":5,"roles":[1,2,3],"uid":10}}
with both the sort objects and the sort arrays option set to true.
In the "real world" you may want to use PHPUnit's "assertJsonStringEqualsJsonString
" function:
<?php use PHPUnit\Framework\TestCase; final class JsonStringEqualsJsonStringTest extends TestCase { public function testSuccess() { $this->assertJsonStringEqualsJsonString( '{"id":10,"usr":{"uid":10,"gid":5,"roles":[3,1,2]},"cat":2}', '{"cat":2,"id":10,"usr":{"gid":5,"roles":[3,1,2],"uid":10}}' ); } }
This test will pass! As it should in accordance with RFC 7159. But wouldn't we have much more fun writing out own canonicalizer for JSON strings? Sure! So, let's go!
To order a JSON array or object we first look for any sub-tree (a child that is an array or an object) and sort those first. After that the entire array or object is sorted. Sorting objects by doing a simple key sort, while sorting arrays is done by converting the sub-tree into a JSON string and doing a normal (case sensitive) string sort. This way the keys at all levels are sorted in lexicographical order. And since we sort the deepest level first and work our way up, we can ensure that the contents stay sorted on all levels. On array sort we take the entire sub-tree into account, ensuring that there are no ties, unless two sub-trees are exactly the same.
Below you find the code for the "json_sort
" function. It takes a JSON string and returns a JSON string as well.
<?php function json_sort(string $json, bool $objects=true, bool $arrays=false): string { // uses a recursive lambda $order = null; $order = function ($json) use (&$order, $objects, $arrays) { // sort sub-trees foreach ($json as $key => $value) { if (is_array($value) || is_object($value)) { if (is_array($json)) { $json[$key] = $order($value); } else { $json->$key = $order($value); } } } // sort this array or object if ($arrays && is_array($json)) { usort($json,function ($a,$b) { return json_encode($a)<=>json_encode($b); }); } elseif ($objects && is_object($json)) { $arr = (array) $json; ksort($arr); $json = (object) $arr; } return $json; }; return json_encode($order(json_decode($json))); }
As always, you can find this code on my Github account:
https://github.com/mevdschee/json_sort.php
Happy coding!
PS: Liked this article? Please share it on Facebook, Twitter or LinkedIn.