2つのJSONをマージするには
既定値の JSON に設定ファイルの JSON を上書きする形のマージを各言語で示す。
浅いマージで入れ子が丸ごと消える問題と、再帰的に統合する書き方の違いまでを扱う。
公開:
各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。
Python 実行確認済み
import json
defaults = json.loads('{"server": {"host": "localhost", "port": 8080}, "debug": false}')
override = json.loads('{"server": {"port": 3000}}')
# | は浅いマージ。server ごと置き換わって host が消える
print(defaults | override) # {'server': {'port': 3000}, 'debug': False}
def deep_merge(base: dict, patch: dict) -> dict:
merged = dict(base)
for key, value in patch.items():
if isinstance(merged.get(key), dict) and isinstance(value, dict):
merged[key] = deep_merge(merged[key], value)
else:
merged[key] = value
return merged
print(deep_merge(defaults, override))
# {'server': {'host': 'localhost', 'port': 3000}, 'debug': False}辞書の | 演算子は同じキーを丸ごと置き換える浅いマージなので、入れ子の一部だけを上書きすると host が消える。
入れ子まで統合したいときは、両方が辞書のときだけ再帰する関数を自分で用意する。
うまくいかない時: Python の dict アクセスで KeyError が解消できない
JavaScript 実行確認済み
const defaults = JSON.parse('{"server": {"host": "localhost", "port": 8080}, "debug": false}');
const override = JSON.parse('{"server": {"port": 3000}}');
// スプレッド構文は浅いマージ。server ごと置き換わって host が消える
console.log({ ...defaults, ...override });
// { server: { port: 3000 }, debug: false }
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
function deepMerge(base, patch) {
const merged = { ...base };
for (const [key, value] of Object.entries(patch)) {
merged[key] =
isPlainObject(merged[key]) && isPlainObject(value)
? deepMerge(merged[key], value)
: value;
}
return merged;
}
console.log(deepMerge(defaults, override));
// { server: { host: 'localhost', port: 3000 }, debug: false }スプレッド構文は最上位のキーだけを見る浅いマージで、値がオブジェクトでもそのまま差し替わる。
入れ子を残したいときは、両辺がプレーンなオブジェクトのときだけ再帰する関数を書く。
配列を統合対象から外すため isPlainObject で Array を弾いている。
TypeScript 実行確認済み
type Json = { [key: string]: unknown };
const defaults = JSON.parse(
'{"server": {"host": "localhost", "port": 8080}, "debug": false}'
) as Json;
const override = JSON.parse('{"server": {"port": 3000}}') as Json;
const isPlainObject = (v: unknown): v is Json =>
typeof v === "object" && v !== null && !Array.isArray(v);
function deepMerge(base: Json, patch: Json): Json {
const merged: Json = { ...base };
for (const [key, value] of Object.entries(patch)) {
const current = merged[key];
merged[key] =
isPlainObject(current) && isPlainObject(value)
? deepMerge(current, value)
: value;
}
return merged;
}
console.log(deepMerge(defaults, override));
// { server: { host: 'localhost', port: 3000 }, debug: false }JSON.parse の戻り値は any なので、扱う形を Json 型として決めてから受け取る。
値が unknown のままでは再帰に渡せないため、型ガードで Json に絞り込んでから deepMerge を呼ぶ。
うまくいかない時: TypeScript で catch の「Object is of type 'unknown'」が解決できない
Go 静的確認
package main
import (
"encoding/json"
"fmt"
)
func deepMerge(base, patch map[string]any) map[string]any {
merged := make(map[string]any, len(base))
for k, v := range base {
merged[k] = v
}
for k, v := range patch {
if inner, ok := merged[k].(map[string]any); ok {
if patchInner, ok := v.(map[string]any); ok {
merged[k] = deepMerge(inner, patchInner)
continue
}
}
merged[k] = v
}
return merged
}
func main() {
var defaults, override map[string]any
json.Unmarshal([]byte(`{"server": {"host": "localhost", "port": 8080}, "debug": false}`), &defaults)
json.Unmarshal([]byte(`{"server": {"port": 3000}}`), &override)
out, _ := json.Marshal(deepMerge(defaults, override))
fmt.Println(string(out))
// {"debug":false,"server":{"host":"localhost","port":3000}}
}map[string]any へ Unmarshal すると入れ子も map[string]any になるので、型アサーションで両方がマップのときだけ再帰する。
マップの代入は参照を渡すため、元の値を壊さないよう新しいマップへ複製してから書き換える。
Rust 静的確認
use serde_json::{json, Value};
fn deep_merge(base: &mut Value, patch: &Value) {
match (base, patch) {
(Value::Object(base_map), Value::Object(patch_map)) => {
for (key, value) in patch_map {
deep_merge(base_map.entry(key.clone()).or_insert(Value::Null), value);
}
}
(base_slot, value) => *base_slot = value.clone(),
}
}
fn main() {
let mut defaults = json!({"server": {"host": "localhost", "port": 8080}, "debug": false});
let override_cfg = json!({"server": {"port": 3000}});
deep_merge(&mut defaults, &override_cfg);
println!("{}", defaults);
// {"debug":false,"server":{"host":"localhost","port":3000}}
}serde_json::Value のまま再帰し、両方がオブジェクトのときだけキー単位で降りていく。
entry(...).or_insert(Value::Null) を挟むと、patch にしか無いキーも同じ経路で書き込める。
serde_json クレートを Cargo.toml に足しておく。
Java 静的確認
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
public class MergeJson {
static ObjectNode deepMerge(ObjectNode base, ObjectNode patch) {
ObjectNode merged = base.deepCopy();
Iterator<String> names = patch.fieldNames();
while (names.hasNext()) {
String key = names.next();
JsonNode current = merged.get(key);
JsonNode value = patch.get(key);
if (current instanceof ObjectNode && value instanceof ObjectNode) {
merged.set(key, deepMerge((ObjectNode) current, (ObjectNode) value));
} else {
merged.set(key, value.deepCopy());
}
}
return merged;
}
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
ObjectNode defaults = (ObjectNode) mapper.readTree(
"{\"server\": {\"host\": \"localhost\", \"port\": 8080}, \"debug\": false}");
ObjectNode override = (ObjectNode) mapper.readTree("{\"server\": {\"port\": 3000}}");
System.out.println(deepMerge(defaults, override));
// {"server":{"host":"localhost","port":3000},"debug":false}
}
}Jackson の ObjectNode を使い、両方がオブジェクトのときだけ再帰して統合する。
set はノードの参照をそのまま持たせるため、元の JSON を後から変更しても影響が出ないよう deepCopy を挟んでいる。
C# 実行確認済み
using System.Text.Json.Nodes;
var defaults = JsonNode.Parse("""{"server": {"host": "localhost", "port": 8080}, "debug": false}""")!.AsObject();
var patch = JsonNode.Parse("""{"server": {"port": 3000}}""")!.AsObject();
static JsonObject DeepMerge(JsonObject baseObj, JsonObject patch)
{
var merged = (JsonObject)baseObj.DeepClone();
foreach (var (key, value) in patch)
{
if (merged[key] is JsonObject inner && value is JsonObject patchInner)
merged[key] = DeepMerge(inner, patchInner);
else
merged[key] = value?.DeepClone();
}
return merged;
}
Console.WriteLine(DeepMerge(defaults, patch).ToJsonString());
// {"server":{"host":"localhost","port":3000},"debug":false}System.Text.Json の JsonNode は追加の依存なしで木構造を組み替えられる。
JsonNode は 1 つの親にしか属せないため、別の木へ移すときは DeepClone を通してから代入する。
つまずき
既定値と設定ファイルを合わせるつもりで浅いマージを使うと、入れ子のオブジェクトが丸ごと置き換わる。
上の例では port だけを上書きしたのに host が消えてしまう。
設定の一部だけを上書きする用途では、ほぼ常に再帰的なマージが要る。
配列はマージしない
ここで示した再帰は、両方がオブジェクトのときだけ降りていき、配列は後勝ちで置き換える。
配列を連結するか置き換えるかは仕様の問題で、正解は用途によって変わる。
連結すると既定値の要素を消せなくなるため、設定の上書きでは置き換えを既定にしておくほうが扱いやすい。
元のデータを壊さない
マージ処理は元のオブジェクトを書き換えてしまいがちで、既定値を使い回す場面で事故になる。
Python の dict(base)、JavaScript のスプレッド、Java の deepCopy、C# の DeepClone はいずれも新しい入れ物を作るためのもので、複製を挟んでから書き込めば呼び出し元の値は変わらない。