{"record":{"id":"2470de582f07b1d9","repo":"nodejs/node","slug":"dictionary-key-is-not-a-string","errorCode":null,"errorMessage":"Dictionary key is not a string.","messagePattern":"Dictionary key is not a string\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"tools/gypi_to_gn.py","lineNumber":149,"sourceCode":"\n    elif isinstance(v, int):\n      yield str(v)\n\n    elif isinstance(v, list):\n      yield '['\n      for i, item in enumerate(v):\n        if i > 0:\n          yield ','\n        for tok in GenerateTokens(item, level + 1):\n          yield tok\n      yield ']'\n\n    elif isinstance(v, dict):\n      if level > 0:\n        yield '{'\n      for key in sorted(v):\n        if not isinstance(key, str):\n          raise ValueError('Dictionary key is not a string.')\n        if not key or key[0].isdigit() or not key.replace('_', '').isalnum():\n          raise ValueError('Dictionary key is not a valid GN identifier.')\n        yield key  # No quotations.\n        yield '='\n        for tok in GenerateTokens(v[key], level + 1):\n          yield tok\n      if level > 0:\n        yield '}'\n\n    else:  # Not supporting float: Add only when needed.\n      raise ValueError('Unsupported type when printing to GN.')\n\n  can_start = lambda tok: tok and tok not in ',}]='\n  can_end = lambda tok: tok and tok not in ',{[='\n\n  # Adds whitespaces, trying to keep everything (except dicts) in 1 line.\n  def PlainGlue(gen):\n    prev_tok = None","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/nodejs/node/blob/1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e/tools/gypi_to_gn.py#L131-L167","documentation":"ValueError raised by GenerateTokens in gypi_to_gn.py when a dict key is not a Python str. GN identifiers must be string tokens, so any int/bool/None key from the GYP data is rejected before emission. This guard runs before the GN-identifier-validity check.","triggerScenarios":"Calling GenerateTokens on a dict whose keys include non-string types - typically the result of JSON parsing with numeric keys, or GYP data transformed by code that converted keys to ints.","commonSituations":"GYP inputs that were round-tripped through JSON or YAML with int keys, or programmatically built dicts using enumerate()/indices as keys.","solutions":["Coerce all dict keys to str before passing the structure to GenerateTokens.","Fix the upstream producer so it emits string keys.","Sanitize the loaded GYP data with a recursive key-stringification pass."],"exampleFix":"# before\ndata = {1: 'x', 'foo': 'y'}\ntokens = list(GenerateTokens(data))\n\n# after\ndef stringify_keys(o):\n    if isinstance(o, dict):\n        return {str(k): stringify_keys(v) for k, v in o.items()}\n    if isinstance(o, list):\n        return [stringify_keys(i) for i in o]\n    return o\ntokens = list(GenerateTokens(stringify_keys(data)))","handlingStrategy":"type-guard","validationCode":"def all_keys_str(obj) -> bool:\n    if isinstance(obj, dict):\n        return all(isinstance(k, str) and all_keys_str(v) for k, v in obj.items())\n    if isinstance(obj, list):\n        return all(all_keys_str(i) for i in obj)\n    return True","typeGuard":"def has_only_string_keys(d: dict) -> bool:\n    return all(isinstance(k, str) for k in d)","tryCatchPattern":"try:\n    list(GenerateTokens(data))\nexcept ValueError as e:\n    if 'not a string' in str(e):\n        data = stringify_keys(data)  # coerce and retry","preventionTips":["Coerce dict keys to str when loading from JSON/YAML.","Sanitize GYP data with a recursive transform before conversion.","Avoid building dicts with numeric keys programmatically."],"tags":["gyp","gn","build","dict","types"],"backgroundTag":null,"analyzedSha":"1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e","analyzedAt":"2026-08-13T00:53:24.642Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}