arrayを含んだ順序が不揃いなJSONのdiff

arrayを含んだ順序が不揃いなJSONのdiffを取るのはだるい。だるい。

arrayを含んだJSON

arrayを含んだJSONというのはこういうやつです。

people0.json

[
  {
    "name": "foo",
    "age": 10
  },
  {
    "name": "bar",
    "age": 20
  }
]

people2.json

[
  {
    "name": "bar",
    "age": 20
  },
  {
    "name": "foo",
    "age": 10
  }
]

JSONを含んだJSON(unordered)のdiffを取りたい

jqの場合

jqにはkeyでソートする -S というオプションはあるのだけれど。これではarrayはソートされない。

$ diff -u <(jq -S . people0.json) -u <(jq -S . people1.json)
--- /dev/fd/63  2017-06-10 15:57:53.000000000 +0900
+++ /dev/fd/62  2017-06-10 15:57:53.000000000 +0900
@@ -1,10 +1,10 @@
 [
   {
-    "age": 10,
-    "name": "foo"
-  },
-  {
     "age": 20,
     "name": "bar"
+  },
+  {
+    "age": 10,
+    "name": "foo"
   }
 ]

これではダメ。まじめに構造を把握していれば、その構造の知識を使ってソートすることは可能(例えば今回の例ではname及びageでソートする)。

$ diff -u <(jq -S "sort_by(.name)" people0.json) <(jq -S "sort_by(.name)" people1.json)

これはOK。ただし、データに対する知識が要求される。

dictknife

だるかったので、dictknifeの --normalize オプションではこれを良い感じでソートしてdiffが出ないようにした。

$ dictknife diff --normalize people0.json people1.json

もちろん、何か変更があればdiffが出る。

$ dictknife diff --normalize people0.json people2.json
--- people0.json
+++ people2.json
@@ -1,10 +1,11 @@
 [
   {
-    "age": 10,
+    "age": 11,
     "name": "foo"
   },
   {
     "age": 20,
-    "name": "bar"
+    "name": "bar",
+    "nickname": "big b"
   }
 ]

もう少し複雑なネストした形状

もう少しネストした複雑な形状のものもある。

例えばこういう(今度はYAML)

people3.yaml

- name: foo
  age: 10
  parents:
    - name: A
      age: 40
    - name: B
      age: 44
- name: bar
  age: 20
  parents:
    - name: B
      age: 44
    - name: A
      age: 40

people4.yaml

- name: bar
  age: 20
  parents:
    - name: B
      age: 44
    - name: A
      age: 40
- name: foo
  age: 10
  parents:
    - name: A
      age: 40
    - name: B
      age: 44

ネストしていても大丈夫。

$ dictknife diff --normalize people3.yaml people4.yaml
$ dictknife diff --normalize people3.yaml people5.yaml
--- people3.yaml
+++ people5.yaml
@@ -6,6 +6,10 @@
       {
         "age": 40,
         "name": "A"
+      },
+      {
+        "age": 42,
+        "name": "C"
       },
       {
         "age": 44,