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
- Quote every `id`/`code` value in the offending layer so all are strings.
- Make the type consistent across the base and override layers (all string, or remove the id field to fall back to concatenation).
- If numeric ids are intentional, drop the `code`/`id` field so the merger treats the arrays as a plain append.
- 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
- Quote all id/code values in TOML so they parse as strings.
- Keep id types consistent across every config layer.
- Drop the id field if you want plain array concatenation.
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
- keyed array identifier `{candidate}` must not be empty
- required TOML file not found: {path}
- TOML layer is not a file: {path}
- failed to parse {path}: {error}
- TOML layer did not parse to a table: {path}
AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13).
Data as JSON: /api/errors/fedc62f4a245c527.
Report an issue: GitHub.