JSON field notes · 03
Compare JSON while ignoring array order
Two API responses can hold the same data in a different order. Object key order never matters in Structura; for arrays you choose between position, an unordered comparison and matching items by an id. The right choice depends on what each array means.
Object key order is always ignored
{"a": 1, "b": 2} and {"b": 2, "a": 1} produce no differences. Properties are matched by name, and indentation, line breaks and spacing between tokens are not compared. You do not need an option for this, and none of the array settings below change it.
One example, four modes
An account export with three arrays: tags (a set of labels), roles (objects with an id) and steps (an ordered checklist). In B, the tags and roles are reordered, a role gains a scope, and two steps swap places.
A · Original
{
"tags": ["beta", "admin", "beta"],
"roles": [
{"id": "r1", "name": "Viewer"},
{"id": "r2", "name": "Editor"}
],
"steps": ["build", "test", "deploy"]
}B · Updated
{
"tags": ["admin", "beta", "beta"],
"roles": [
{"id": "r2", "name": "Editor", "scope": "all"},
{"id": "r1", "name": "Viewer"}
],
"steps": ["build", "deploy", "test"]
}| Options | Changes | What the report shows |
|---|---|---|
| Default (by position) | 9 | tags[0], tags[1]; id and name of both roles plus an added scope at roles[0]; steps[1], steps[2]. Accurate but noisy: the roles look like they were edited. |
| Ignore array order | 1 | $["roles"] changed as a whole. Tags are equal as a collection. The swapped steps are hidden, because this option applies to every array. |
Match array items by id | 5 | $["roles"][?@["id"]=="r2"]["scope"] added. Tags and steps are still compared by position, so they report 2 changes each. |
| Both options | 1 | Only the added scope. Roles are paired by id; tags and steps are compared without order, so the step swap is again hidden. |
The most useful view here is Match array items by id: the role change is named exactly, and the step swap stays visible. Ignore array order is the right choice only when no array in the document has a meaningful order.
Which mode to use
| Your arrays hold | Use | Because |
|---|---|---|
| Ordered data: steps, rankings, time series, coordinates | Position (default) | A move is a real change and should be reported. |
| Records with an identifier: users, line items, roles | Match array items by id (or sku, uuid…) | Each record is compared with its own counterpart, and a deletion does not shift the rest. |
| Sets of plain values: tags, permissions, feature flags | Ignore array order | Only membership matters, and duplicates still count. |
| A mix, with no ordered arrays that matter | Both | Records are paired by id; other arrays are compared as collections. |
If timestamps or request ids differ on every response, add them to Ignore keys as well. Ignored keys are skipped inside unordered arrays too, so two items that differ only in updatedAt count as equal.
Exact rules
- Ignore array order compares each array as a collection that keeps duplicates:
[1, 1, 2]differs from[1, 2, 2]. When the collections differ, the whole array is one change with its before and after values. - Match array items by takes one property name. It applies to an array at any depth only when every item on both sides is an object with that property, holding a string or number, and no value repeats on either side.
- Ids keep their type: the number
1and the string"1"are different ids. - An array that does not meet these conditions falls back to position, or to an unordered comparison if that option is on. The note above the results says how many arrays were matched by id.
- Paths for matched items use the JSONPath filter form from RFC 9535, for example
$["roles"][?@["id"]=="r2"], so they name the item rather than a position that has moved. - A JSON Patch is not offered while items are matched by id; see the JSON Patch generator notes.
Alternative: sort before comparing
If you need the comparison in a script, you can normalise both files first and then compare them as text. With jq, -S sorts object keys and walk sorts every array:
jq -S 'walk(if type == "array" then sort else . end)' a.json > a.sorted.json jq -S 'walk(if type == "array" then sort else . end)' b.json > b.sorted.json diff a.sorted.json b.sorted.json
This has the same drawback as Ignore array order: every array loses its order, including ones where order matters. And a changed record sorts to a different position, so a text diff shows it as removed and added instead of naming the field that changed.
Questions
Does JSON object key order affect the comparison?
No. Object properties are matched by name in every mode, and whitespace and indentation are ignored. Only arrays have an order that can matter.
Can I ignore order for one array but not another?
Not with Ignore array order, which applies to every array at every depth. Match array items by is more selective: it only pairs arrays in which every item on both sides has a unique value for that property, and other arrays keep the positional or unordered setting.
Are duplicate array items counted when order is ignored?
Yes. Arrays are compared as collections that keep duplicates, so [1, 1, 2] and [1, 2, 2] are different, while [1, 2, 2] and [2, 1, 2] are the same.
Why does an unordered array show as one change?
Without positions or ids there is no reliable way to say which item became which, so the whole array is reported once with its before and after values. Use Match array items by when the items have an identifier.
Are 1 and "1" treated as the same id?
No. Ids keep their JSON type, so the number 1 and the string "1" are different ids, just as they are different values elsewhere in the report.