deepMerge Transform
Deep recursive merge where later inputs override earlier ones at the leaf level. Type conflicts are allowed (later value wins).
Usage
Using in-memory data (recommended):
node:
base:
from_file: base.json
override:
from_file: override.json
config:
to_file: config.json
transform:
deepMerge:
- "{{.base}}"
- "{{.override}}"
Using file paths (via .meta):
node:
base:
from_file: base.json
override:
from_file: override.json
config:
to_file: config.json
transform:
deepMerge:
- "{{.meta.base.path}}"
- "{{.meta.override.path}}"
Behavior
| Scenario |
Result |
| Objects |
Keys are merged recursively |
| Arrays |
Later array replaces earlier array |
| Scalars |
Later value replaces earlier value |
| Type conflicts |
Later type wins (e.g., string can become object) |
Examples
Basic Merge
base.json:
{
"name": "app",
"settings": {
"debug": false,
"port": 8080
}
}
override.json:
{
"settings": {
"debug": true,
"timeout": 30
}
}
Result:
{
"name": "app",
"settings": {
"debug": true,
"port": 8080,
"timeout": 30
}
}
Type Override
deepMerge allows changing types. If base.json has "value": "string" and override.json has "value": {"nested": "object"}, the result will have the object.
Array Replacement
Arrays are not merged element-by-element. The entire array is replaced:
base.json:
override.json:
Result:
Multiple Files
You can merge more than two files. They are processed in order:
transform:
deepMerge:
- "{{.defaults}}"
- "{{.environment}}"
- "{{.local}}"
With Multi-Document Nodes
For nodes with multiple files or glob inputs, {{.nodeName}} expands to all documents in sorted order:
node:
configs:
from_file:
- base.yaml
- env.yaml
- local.yaml
merged:
to_file: merged.json
transform:
deepMerge:
- "{{.configs}}" # Expands to all 3 documents
Or access specific documents:
transform:
deepMerge:
- "{{.configs.base}}"
- "{{.configs.env}}"
- "{{.configs.local}}"
Field Access
Access individual fields from documents instead of entire documents:
node:
config:
from_file: config.json # {"database": {...}, "cache": {...}, "api": {...}}
override:
from_file: override.json
out:
to_file: db.json
transform:
deepMerge:
- "{{.config.database}}" # Only the database section
- "{{.override}}"
Nested field access is supported:
transform:
deepMerge:
- "{{.config.services.api.database}}" # Deeply nested field
- "{{.override}}"
For multi-document nodes, specify document name first, then field path:
node:
configs:
from_glob: "configs/*.json" # base.json, prod.json
out:
transform:
deepMerge:
- "{{.configs.base.database}}" # 'database' field in base doc
- "{{.configs.prod.database}}" # 'database' field in prod doc
Error handling:
- Missing field:
"field 'nonexistent' not found in node 'config'"
- Non-object value:
"field 'port' is not an object (got float64), cannot use in merge transform"
With Glob Inputs
Glob nodes work the same way - documents are named by filename stem:
node:
configs:
from_glob: "configs/*.yaml"
merged:
to_file: merged.json
transform:
deepMerge:
- "{{.configs}}" # All matched files in sorted order
With Intermediate Nodes
Reference other nodes (processed in dependency order):
node:
base1:
from_file: base1.yaml
base2:
from_file: base2.yaml
overrides:
from_file: overrides.yaml
# Intermediate node
base_merged:
transform:
deepMerge:
- "{{.base1}}"
- "{{.base2}}"
# Final output
final:
to_file: config.json
transform:
deepMerge:
- "{{.base_merged}}"
- "{{.overrides}}"
CLI Usage
Run deepMerge directly from the command line:
panconf transform deepMerge base.json override.yaml extra.toml
Output is YAML to stdout.