A lightweight Go library and CLI tool for merging configuration files with intelligent list handling.
Configuration management often requires layering multiple config files (base + environment-specific overlays). Simple merging breaks when you have lists of objects that should be matched and merged intelligently rather than concatenated or replaced wholesale.
keymerge solves this by:
- Matching list items by primary keys (
id,name, etc.) and deep-merging them - Supporting deletion of specific items from base configs
- Working with any format (YAML, JSON, TOML) or pre-parsed data structures
- Providing zero-dependency, production-ready code with >90% test coverage
Here's a real-world example: merging a base config with production defaults, then applying customer-specific overrides. Shows deletion markers, tag deduplication, and format conversion.
Watch for: Services matched by name and deep-merged (api: 1→10→25 replicas), debug-proxy removed via _delete marker, tags deduplicated (us-east appears only once), and cross-format merging (YAML/JSON/TOML → JSON).
base.yaml:
database:
host: localhost
pool_size: 10
timeout: 30s
services:
- name: api
replicas: 1
port: 8080
- name: worker
replicas: 1
enabled: true
- name: debug-proxy
replicas: 1
port: 9000
features:
- rate-limiting
- metrics
tags:
- default
- apiprod.json (different format - showing format-agnostic merging):
{
"database": {
"host": "prod.db.example.com",
"pool_size": 50,
"timeout": "60s"
},
"services": [
{
"name": "api",
"replicas": 10
},
{
"name": "worker",
"replicas": 5
},
{
"name": "debug-proxy",
"_delete": true
}
],
"features": [
"tracing",
"audit-logging"
],
"tags": [
"production",
"us-east"
]
}customer1.toml:
features = ["premium-support", "custom-reports"]
tags = ["customer1", "us-east", "premium"]
[database]
pool_size = 100
read_replicas = 3
[[services]]
name = "api"
replicas = 25
cache_enabled = trueMerge them:
cfgmerge -scalar dedup -format json -out config.json base.yaml prod.json customer1.tomlResult (config.json):
{
"database": {
"host": "prod.db.example.com",
"pool_size": 100,
"timeout": "60s",
"read_replicas": 3
},
"services": [
{
"name": "api",
"replicas": 25,
"port": 8080,
"cache_enabled": true
},
{
"name": "worker",
"replicas": 5,
"enabled": true
}
],
"features": [
"rate-limiting",
"metrics",
"tracing",
"audit-logging",
"premium-support",
"custom-reports"
],
"tags": [
"default",
"api",
"production",
"us-east",
"customer1",
"premium"
]
}What happened:
- Services matched by
nameand deep-merged (api: 1→10→25 replicas, worker: 1→5, debug-proxy removed) - debug-proxy removed via
_delete: truemarker in prod layer - Database settings progressively tuned (pool_size: 10→50→100, read_replicas added for customer)
- Features concatenated across all layers (base→prod→customer features combined)
- Tags deduplicated (via
-scalar dedupflag - "us-east" appears only once) - Format conversion: YAML + JSON + TOML → JSON output
CLI tools:
# Config file merger
go install github.com/sam-fredrickson/keymerge/cmd/cfgmerge@latest
# Kustomize KRM function
go install github.com/sam-fredrickson/keymerge/cmd/cfgmerge-krm@latest
# Or download pre-built binaries from releases
# https://github.com/sam-fredrickson/keymerge/releases
# Or use Docker
docker pull samuelfredrickson/cfgmerge:latestGo library:
go get github.com/sam-fredrickson/keymergeRequires Go 1.24 or later.
Use cfgmerge in an initContainer to merge base and environment-specific configs at deployment time:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-configs
data:
base.yaml: |
database:
host: localhost
port: 5432
pool_size: 10
timeout: 30s
features:
- name: rate-limiting
enabled: true
limit: 1000
- name: caching
enabled: false
log_level: info
production.yaml: |
database:
host: prod-db.default.svc.cluster.local
pool_size: 50
timeout: 60s
features:
- name: rate-limiting
limit: 10000
- name: caching
enabled: true
ttl: 300
log_level: warn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
initContainers:
- name: merge-config
image: samuelfredrickson/cfgmerge:latest
args:
- -out
- /config/app-config.yaml
- /configs/base.yaml
- /configs/production.yaml
volumeMounts:
- name: config-sources
mountPath: /configs
- name: merged-config
mountPath: /config
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: merged-config
mountPath: /etc/myapp
# App reads merged config from /etc/myapp/app-config.yaml
volumes:
- name: config-sources
configMap:
name: app-configs
- name: merged-config
emptyDir: {}This pattern keeps your base config in version control and environment-specific overrides in ConfigMaps, merging them at runtime.
Want to customize? Run cfgmerge -h to see all options: custom primary keys (-keys), list merge modes (-scalar, -dupe), deletion markers (-delete-marker), and more.
Use cfgmerge-krm as a Kustomize transformer to merge ConfigMaps declaratively using annotations:
# base/config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-base-config
annotations:
config.keymerge.io/id: "app-config"
config.keymerge.io/order: "0"
config.keymerge.io/final-name: "app-config"
data:
app-config.yaml: |
server:
port: 8080
logging:
level: info# features/tracing/config-snippet.yaml (Kustomize Component)
apiVersion: v1
kind: ConfigMap
metadata:
name: tracing-config
annotations:
config.keymerge.io/id: "app-config"
config.keymerge.io/order: "10"
data:
app-config.yaml: |
tracing:
enabled: true
endpoint: http://jaeger:14268/api/traces# envs/dev/config-env.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dev-overlay
annotations:
config.keymerge.io/id: "app-config"
config.keymerge.io/order: "100"
data:
app-config.yaml: |
server:
port: 3000
logging:
level: debugConfigure the transformer in your kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- config-env.yaml
components:
- ../../features/tracing
transformers:
- transformer-config.yaml# transformer-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cfgmerge-transformer
annotations:
config.kubernetes.io/function: |
exec:
path: cfgmerge-krmBuild with:
kustomize build --enable-alpha-plugins --enable-exec envs/devResult: Single merged ConfigMap with base config, tracing feature, and dev overrides applied in order.
See the full example: examples/kustomize/ includes a complete working setup with base, features, and environment overlays.
For programmatic config merging in Go:
import (
"github.com/sam-fredrickson/keymerge"
"github.com/goccy/go-yaml"
)
type Config struct {
Database Database `yaml:"database"`
Services []Service `yaml:"services"`
}
type Service struct {
Name string `yaml:"name" km:"primary"`
Replicas int `yaml:"replicas"`
}
merger, _ := keymerge.NewMerger[Config](keymerge.Options{}, yaml.Unmarshal, yaml.Marshal)
result, _ := merger.Merge(baseConfig, prodOverlay)The km:"primary" struct tag marks Name as the primary key for matching service items during merge.
Two API styles:
NewMerger[T]- Type-safe with struct tags (recommended)NewUntypedMerger- Dynamic for runtime configs
See the User Guide for comprehensive examples, patterns, and advanced features like composite keys, field-specific merge modes, and error handling.
- User Guide - Comprehensive examples, patterns, and best practices
- API Reference - Complete API documentation
Apache 2.0 - see LICENSE
If keymerge doesn't fit your use case, consider:
- uber/config - YAML-based config with advanced merging (requires all inputs to be YAML)
- kustomize - Kubernetes-native config management (K8s-specific, strategic merge patches)
- yq - YAML/JSON/XML processing (manual scripting required for complex merges)
keymerge's niche is format-agnostic merging with intelligent list matching - if you need primary-key-based list merging across YAML/JSON/TOML, this is the tool.