ネストした辞書(JSON)のマージについて考えるときに気にするのはリスト(Array)に対する操作かもしれない

github.com

JSONのマージ

dictknifeにはcatというサブコマンドがあり、これに複数のファイルを指定すると良い感じにマージしてくれる。

例えば以下のような2つのjsonをマージしてみる(mkdictはjoのようなコマンドラインJSONをつくるための便利コマンド。ファイルにするのが面倒だったので使っているだけ。特に意味はない)。

$ dictknife mkdict name=foo age=20 | tee person.json
{
  "name": "foo",
  "age": 20
}
$ dictknife mkdict nickname=F | tee nickname.json
{
  "nickname": "F"
}
$ dictknife cat person.json nickname.json -o json
{
  "name": "foo",
  "age": 20,
  "nickname": "F"
}

キーに衝突があった場合には後勝ちで後ろにあるものが優先される。これは設定ファイルを一部変更して使うことなどを考えると自然な形(だと思う)。

$ dictknife mkdict name=foo age=20 | tee person.json
{
  "name": "foo",
  "age": 20
}
$ dictknife mkdict name=FOO nickname=F | tee nickname2.json
{
  "name": "FOO",
  "nickname": "F"
}
$ dictknife cat person.json nickname2.json -o json
{
  "name": "FOO",
  "age": 20,
  "nickname": "F"
}

ネストしたJSONのマージ

もちろんネストしたJSONにも使える。

$ dictknife mkdict father/name=bar mother/name=boo | tee parents.json
{
  "father": {
    "name": "bar"
  },
  "mother": {
    "name": "boo"
  }
}
$ dictknife mkdict mother/name=xyz | tee parents2.json
{
  "mother": {
    "name": "xyz"
  }
}
$ dictknife cat person.json parents.json parents2.json -o json
{
  "name": "foo",
  "age": 20,
  "father": {
    "name": "bar"
  },
  "mother": {
    "name": "xyz"
  }
}

内部的にはcollections.ChainMapを使うと値の欠損がないだとか、元の値を壊さないようにするべきかとか色々考えることはあるけれど。こうやってコマンドとして使う分には気にする必要はなさそう。

悩みどころはJSONのArray(list)

内部にリストを持っている時に何が自然か?というのが結構シチュエーション毎に違う。例えば

  • [1,2,3,4,5]
  • [2,4,6]

を値の要素に持つ辞書をマージしたときにはどの様になるのが自然と言えるんだろうか?

addtoset(default)

集合の様に考えてマージすると [1,2,3,4,5,6] になる。これがデフォルトの形。設定ファイルなどをいじっていて追加で設定を加えたい場合などにはこの形が便利。

$ dictknife --log=WARNING --compact  mkdict vs/=1 vs/=2 vs/=3 vs/=4 vs/=5 | tee xs.json
{"vs": [1, 2, 3, 4, 5]}
$ dictknife --log=WARNING --compact  mkdict vs/=2 vs/=4 vs/=6 | tee ys.json
{"vs": [2, 4, 6]}
$ dictknife --log=WARNING --compact cat xs.json ys.json -o json
{"vs": [1, 2, 3, 4, 5, 6]}

replace

衝突したときの値を完全に置き換えたい場合もある。元の値から要素を取り除いて完全に新しい値で置き換えたい場合はもある。このときは[2,4,6]になる。

例えばテンプレートエンジン(jinja2)に渡す設定値のところではこれがデフォルトだと嬉しかった(ちなみにkamidanaのデフォルト)。

他の方法でマージするには--merge-methodを指定する(まだmaster branchでしか動かない)。

$ dictknife --log=WARNING --compact mkdict vs/=1 vs/=2 vs/=3 vs/=4 vs/=5 | tee xs.json
{"vs": [1, 2, 3, 4, 5]}
$ dictknife --log=WARNING --compact mkdict vs/=2 vs/=4 vs/=6 | tee ys.json
{"vs": [2, 4, 6]}
$ dictknife --log=WARNING --compact cat --merge-method=replace xs.json ys.json -o json
{"vs": [2, 4, 6]}

append

特に置き換えたりせず全ての値を取り込んで返したい場合もある。リストにおけるappendというかextend的な操作。このときは[1, 2, 3, 4, 5, 2, 4, 6]になる。

$ dictknife --log=WARNING --compact mkdict vs/=1 vs/=2 vs/=3 vs/=4 vs/=5 | tee xs.json
{"vs": [1, 2, 3, 4, 5]}
$ dictknife --log=WARNING --compact mkdict vs/=2 vs/=4 vs/=6 | tee ys.json
{"vs": [2, 4, 6]}
$ dictknife --log=WARNING --compact cat --merge-method=merge xs.json ys.json -o json
{"vs": [1, 2, 3, 4, 5, 2, 4, 6]}

merge

2つの値をそのまま繋げたいのだけれど。先程のappendが縦方向(?)の結合であったとしたら横方向の結合(?)をしたくなることもある。この操作に関しては先程までの要素が数値だけの例だと不適切かもしれない。例えば同時に指定できるフィールドの数に制限があり、N回に分けたrequestのresponseをマージしたいような状況等で使う。

$ cat values.json
[
  {"name": "damage", "value": 100},
  {"name": "heal", "value": 200},
  {"name": "damage", "value": 100}
]
$ cat values2.json
[
  {"name": "damage", "id": 1},
  {"name": "heal", "id": 2},
  {"name": "damage", "id": 3}
]
$ dictknife cat --merge-method=merge values.json values2.json -o json
[
  {
    "name": "damage",
    "value": 100,
    "id": 1
  },
  {
    "name": "heal",
    "value": 200,
    "id": 2
  },
  {
    "name": "damage",
    "value": 100,
    "id": 3
  }
]

今の所これに対して重複を取り除きたい(addtoset的なふるまいをさせたい)という気持ちになったことはない。

ところで

どこかのタイミングでjoinだとかpandas的な操作だとかが欲しくなってくるような気がしている(とはいえカジュアルに使うにはpandasのimportは重いし。こちらは主にデータのハンドリングというよりは設定ファイルのような小さめの設定を取り扱うことをイメージしている)。