{
  "slug": "config-merge",
  "name": "gizza-ai/config-merge",
  "version": "0.1.0",
  "title": "Config Merge Tool — Layer JSON, YAML, TOML & env Files — gizza.ai",
  "description": "Merge layered JSON, YAML, TOML and .env configuration files with override precedence, array strategy, null deletion and variable substitution.",
  "tags": [
    "config merge",
    "configuration merge",
    "json merge",
    "yaml merge",
    "toml merge",
    "env merge",
    "dotenv merge",
    "layered config",
    "deep merge",
    "variable substitution",
    "config override",
    "settings merge"
  ],
  "category": "documents",
  "urls": {
    "page": "https://gizza.ai/tools/config-merge/",
    "markdown": "https://gizza.ai/tools/config-merge/index.md",
    "descriptor": "https://gizza.ai/tools/config-merge/tool.json",
    "deep_link_example": "https://gizza.ai/tools/config-merge/?layer1=%7B%0A%20%20%22db%22%3A%20%7B%20%22host%22%3A%20%22localhost%22%2C%20%22port%22%3A%205432%20%7D%2C%0A%20%20%22debug%22%3A%20false%0A%7D&layer2=db%3A%0A%20%20host%3A%20staging.internal&layer3=DEBUG%3Dtrue%0ADB__PORT%3D6543&layer4=debug%20%3D%20false&layer_names=defaults.json%2Cstaging.yaml%2C.env%2Clocal.toml&input_format=auto&output=json&object_merge=deep&array_merge=replace&key_case=match&null_deletes=true&substitute=true&vars=DB_USER%3Dsvc%0ADB_PASSWORD%3D%2A%2A%2A&sort_keys=true&indent=2"
  },
  "cli": "gizza tool config-merge '{\n  \"db\": { \"host\": \"localhost\", \"port\": 5432 },\n  \"debug\": false\n}'",
  "tool": {
    "description": "Merge up to four layered configuration files into one document, even when the layers are in DIFFERENT formats — a JSON base, a YAML staging override and a .env deployment layer merge in a single pass. Each layer's syntax (JSON, YAML, TOML or .env) is detected automatically, or forced with input_format. Layers apply left to right: layer1 is the base and layer4 has the final say. Mappings merge recursively, lists are replaced (or appended / de-duplicated), and a key set to null in a later layer deletes it. After merging, ${VAR}, ${VAR:-default} and ${VAR-default} references inside string values are expanded against the 'vars' list first and then against the merged document itself; unresolvable references are left verbatim and reported. Flat env keys split on '__' into nested paths (DB__HOST=x becomes db.host). Emits json, yaml, toml, env, or a report naming the layer that set each value plus the override chain. Comments and YAML anchors are not preserved. Max 256 KiB of input across all layers. Runs locally.",
    "parameters": {
      "additionalProperties": false,
      "properties": {
        "array_merge": {
          "default": "replace",
          "description": "How lists combine: 'replace' (default) = the later layer's list wins outright; 'append' = concatenate base then override; 'unique' = concatenate then drop items already present.",
          "enum": [
            "replace",
            "append",
            "unique"
          ],
          "type": "string"
        },
        "indent": {
          "default": 2,
          "description": "Indentation in spaces per nesting level for the json and yaml outputs (1-8). Default 2. Ignored by toml, env and report.",
          "maximum": 8,
          "minimum": 1,
          "type": "integer"
        },
        "input_format": {
          "default": "auto",
          "description": "Syntax of the layers. 'auto' (default) sniffs each layer independently: valid JSON starting with { or [ is JSON; a [section] header means TOML; a block of KEY=value lines with no spaces around '=' is .env; TOML's 'key = value' spacing means TOML; anything else is parsed as YAML. Set json/yaml/toml/env to force ONE syntax for every layer when a file would otherwise be sniffed wrong.",
          "enum": [
            "auto",
            "json",
            "yaml",
            "toml",
            "env"
          ],
          "type": "string"
        },
        "key_case": {
          "default": "match",
          "description": "How later layers match keys whose spelling differs only by case. 'match' (default) lets an env key such as DB__HOST override db.host while keeping the base layer's spelling. 'preserve' treats DB and db as separate keys.",
          "enum": [
            "match",
            "preserve"
          ],
          "type": "string"
        },
        "layer1": {
          "description": "The LOWEST-priority layer — the base config file (e.g. config.json, defaults.yaml, Cargo-style config.toml or a .env). Its syntax is detected automatically unless input_format says otherwise. Must be a mapping at the top level.",
          "type": "string"
        },
        "layer2": {
          "description": "Second layer; its keys override layer1 (e.g. config.staging.yaml). May be in a DIFFERENT format than layer1 — a YAML override on a JSON base is fine. Leave blank if this file doesn't exist; a blank layer is skipped, not an error.",
          "type": "string"
        },
        "layer3": {
          "description": "Third layer; its keys override layer1 and layer2 (e.g. config.local.toml). Leave blank if absent.",
          "type": "string"
        },
        "layer4": {
          "description": "Fourth and HIGHEST-priority layer; its keys override every other layer. Paste the deployment's .env or CI variables here to model 'the environment beats the files'. Leave blank if absent.",
          "type": "string"
        },
        "layer_names": {
          "description": "Comma-separated display names for the four slots, used by output=report to say which file set each value (e.g. 'config.json,config.prod.yaml,.env'). Names map positionally; blank entries fall back to base, override, environment, local.",
          "type": "string"
        },
        "null_deletes": {
          "default": true,
          "description": "true (default) = a key set to null in a later layer REMOVES it from the result, so an override file can delete an inherited default; false = null is kept as an ordinary value.",
          "type": "boolean"
        },
        "object_merge": {
          "default": "deep",
          "description": "How nested mappings combine: 'deep' (default) merges them recursively key by key, so an override file only needs the keys it changes; 'shallow' merges top-level keys only and replaces the whole value below them.",
          "enum": [
            "deep",
            "shallow"
          ],
          "type": "string"
        },
        "output": {
          "default": "json",
          "description": "Format of the merged result: 'json' (default), 'yaml', 'toml' (nulls are dropped — TOML has none), 'env' (flattened UPPER_CASE keys joined with __, e.g. DB__HOST=localhost), or 'report' — the resolved values annotated with the layer that set each one, plus the override chain, keys removed by null, and every variable that was substituted or left unresolved.",
          "enum": [
            "json",
            "yaml",
            "toml",
            "env",
            "report"
          ],
          "type": "string"
        },
        "sort_keys": {
          "default": false,
          "description": "false (default) = keep the base layer's key order, then newly added keys in the order the later layers introduced them; true = sort every mapping's keys A-Z recursively, which makes two merged results diffable.",
          "type": "boolean"
        },
        "substitute": {
          "default": true,
          "description": "true (default) = after merging, expand ${VAR}, ${VAR:-default} (default when unset OR empty) and ${VAR-default} (default only when unset) inside string values, resolving against 'vars' first and then the merged document itself (${db.host} or ${DB__HOST}). '$$' is a literal '$'; a bare $VAR is left alone; an unresolvable reference is left exactly as written and listed under Unresolved in output=report. false = leave every ${...} untouched.",
          "type": "boolean"
        },
        "vars": {
          "description": "Extra substitution variables as KEY=VALUE lines (one per line, quotes and # comments allowed), e.g. 'DB_PASSWORD=s3cr3t'. They outrank values from the merged document and are used ONLY for substitution — they never appear in the output, which is how secrets stay out of the merged file.",
          "type": "string"
        }
      },
      "required": [
        "layer1"
      ],
      "type": "object"
    }
  }
}