bmad-code-org/BMAD-METHOD · error · ConfigError

keyed array identifier `{candidate}` must be a string, got {

Error message

keyed array identifier `{candidate}` must be a string, got {type(value).__name__}

What it means

During `structural_merge`, `_detect_keyed_merge_field` decides whether two arrays should be merged by identity (using a `code` or `id` key on every element) or simply concatenated. If every element has the candidate field but at least one value is not a string, this error is raised — keyed identity requires stable string keys. A non-string id (an integer, a bool, a list) would produce unreliable dedup because Python dict keys of different types can collide silently.

Source

Thrown at src/scripts/config_utils.py:45

            parsed = tomllib.load(stream)
    except tomllib.TOMLDecodeError as error:
        raise ConfigError(f"failed to parse {path}: {error}") from error
    except OSError as error:
        raise ConfigError(f"failed to read {path}: {error}") from error
    if not isinstance(parsed, dict):
        raise ConfigError(f"TOML layer did not parse to a table: {path}")
    return parsed


def _detect_keyed_merge_field(items: list[Any]) -> str | None:
    if not items or not all(isinstance(item, dict) for item in items):
        return None
    for candidate in _KEYED_MERGE_FIELDS:
        if all(candidate in item for item in items):
            for item in items:
                value = item[candidate]
                if not isinstance(value, str):
                    raise ConfigError(
                        f"keyed array identifier `{candidate}` must be a string, "
                        f"got {type(value).__name__}"
                    )
                if not value:
                    raise ConfigError(
                        f"keyed array identifier `{candidate}` must not be empty"
                    )
            return candidate
    return None


def _merge_arrays(base: list[Any], override: list[Any]) -> list[Any]:
    keyed_field = _detect_keyed_merge_field(base + override)
    if keyed_field is None:
        return list(base) + list(override)

    result: list[Any] = []
    index_by_key: dict[str, int] = {}

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Quote every `id`/`code` value in the offending layer so all are strings.
  2. Make the type consistent across the base and override layers (all string, or remove the id field to fall back to concatenation).
  3. If numeric ids are intentional, drop the `code`/`id` field so the merger treats the arrays as a plain append.
  4. Find the bad entry via the reported type and the file you last edited.

Example fix

# before (customize.user.toml)
[[review_layers]]
id = 3
name = "security"

# after
[[review_layers]]
id = "3"
name = "security"
Defensive patterns

Strategy: type-guard

Validate before calling

def keyed_ids_are_strings(items):
    for cand in ("code","id"):
        if all(isinstance(i,dict) and cand in i for i in items):
            return all(isinstance(i[cand], str) for i in items)
    return True

Type guard

def has_string_ids(items: list) -> bool:
    return all(isinstance(i.get('id') if isinstance(i,dict) else None, str) for i in items) if items else True

Try / catch

from config_utils import ConfigError
try:
    structural_merge(base, override)
except ConfigError as e:
    print(f"error: {e}", file=sys.stderr); sys.exit(2)

Prevention

When it happens

Trigger: A customization TOML defines `[[...]]` entries where `id` or `code` is an integer (`id = 3`) or a boolean, while the base layer has string ids. Mixed types across layers also trigger it because the check inspects `base + override`.

Common situations: A schema drift where ids were numeric in one layer and string in another; importing ids from a database as integers; a hand-edit that dropped quotes off an id.

Related errors


AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13). Data as JSON: /api/errors/fedc62f4a245c527. Report an issue: GitHub.