schemagen emits type names starting with a digit for property keys beginning with a digit (generated code does not compile) #59

Open
opened 2026-08-16 07:01:16 +00:00 by weaselbot · 0 comments
Member

Summary

weaseljson_schemagen.py generates C++ type names (enum classes and structs) that start with a digit when a JSON Schema property key, $defs name, or array hint begins with a digit. Such names are not valid C++ identifiers, so the generated header fails to compile. This happens for perfectly valid JSON Schemas — JSON object keys may be any string, including "3", "2c", "2024", etc.

Root cause

Type names are produced by camel() and unique_name() in contrib/schemagen/weaseljson_schemagen.py:

def camel(name):                       # line 153
    parts = [p for p in name.replace("-", " ").replace("_", " ").split(" ") if p]
    if not parts:
        return "T"
    return "".join(p[:1].upper() + p[1:] for p in parts)

camel("3") returns "3" and camel("2c") returns "2c". The member-name helper sanitize() (line 111) explicitly fixes this case:

    if s[0].isdigit():
        s = "_" + s

but camel() and unique_name() (line 251) never apply that check, so the digit-leading name is emitted verbatim as an enum/struct type name.

Reproduction

Minimal schema (schema.json):

{"type":"object","additionalProperties":false,"properties":{"3":{"enum":["a","b"]}}}
python3 contrib/schemagen/weaseljson_schemagen.py schema.json -o gen.h --namespace ts

schemagen exits 0 and emits (note enum class 3, std::optional<3>, 3_names):

enum class 3 : int { a, b };
struct Root1;
struct Root1 {
  std::optional<3> _3;  // "3"
};
...
static constexpr const char *3_names[] = { "a", "b" };

Compiling any TU that includes gen.h fails:

gen.h:17:12: error: expected identifier before numeric constant
   17 | enum class 3 : int { a, b };
      |            ^

The same defect affects generated struct names. For example:

{"type":"object","additionalProperties":false,"properties":{"2c":{"type":"array","items":{"type":"array","items":{"type":"object","additionalProperties":false,"properties":{"x":{"type":"string"}}}}}}}

emits:

struct 2cItemItem;            // invalid: starts with a digit
struct 2cItemItem { ... };
enum class Kind : uint8_t { Root1, 2cItemItem, Arr0, Arr1 };   // invalid enumerator
std::optional<std::vector<std::vector<2cItemItem>>> _2c;       // invalid

which also fails to compile:

gen.h:18:8: error: expected identifier before numeric constant
   18 | struct 2cItemItem;

Expected vs actual

  • Expected: schemagen should produce a compilable header for any valid JSON Schema whose property/$defs names start with a digit (e.g. by prefixing the generated type name with _ when it begins with a digit, the way sanitize() already does for member names).
  • Actual: schemagen exits 0 but emits type names beginning with a digit, and the generated header does not compile.

Impact

Any schema using a digit-leading property key with an enum value, or a digit-leading key wrapping arrays-of-arrays-of-objects (and similar hint derivations), yields non-compiling generated code. Digit-leading keys are common and valid (e.g. "123", "2024", "2c"), so this is reachable with ordinary schemas, not only adversarial ones.

Files / lines

  • contrib/schemagen/weaseljson_schemagen.py:153camel() returns digit-leading names.
  • contrib/schemagen/weaseljson_schemagen.py:251unique_name() does not sanitize a leading digit.
  • contrib/schemagen/weaseljson_schemagen.py:111-119sanitize() shows the existing correct handling for member names (if s[0].isdigit(): s = "_" + s), which is not applied to generated type names.
## Summary `weaseljson_schemagen.py` generates C++ type names (enum classes and structs) that **start with a digit** when a JSON Schema property key, `$defs` name, or array `hint` begins with a digit. Such names are not valid C++ identifiers, so the generated header fails to compile. This happens for perfectly valid JSON Schemas — JSON object keys may be any string, including `"3"`, `"2c"`, `"2024"`, etc. ## Root cause Type names are produced by `camel()` and `unique_name()` in `contrib/schemagen/weaseljson_schemagen.py`: ```python def camel(name): # line 153 parts = [p for p in name.replace("-", " ").replace("_", " ").split(" ") if p] if not parts: return "T" return "".join(p[:1].upper() + p[1:] for p in parts) ``` `camel("3")` returns `"3"` and `camel("2c")` returns `"2c"`. The member-name helper `sanitize()` (line 111) explicitly fixes this case: ```python if s[0].isdigit(): s = "_" + s ``` but `camel()` and `unique_name()` (line 251) never apply that check, so the digit-leading name is emitted verbatim as an enum/struct type name. ## Reproduction Minimal schema (`schema.json`): ```json {"type":"object","additionalProperties":false,"properties":{"3":{"enum":["a","b"]}}} ``` ```sh python3 contrib/schemagen/weaseljson_schemagen.py schema.json -o gen.h --namespace ts ``` `schemagen` exits 0 and emits (note `enum class 3`, `std::optional<3>`, `3_names`): ```cpp enum class 3 : int { a, b }; struct Root1; struct Root1 { std::optional<3> _3; // "3" }; ... static constexpr const char *3_names[] = { "a", "b" }; ``` Compiling any TU that includes `gen.h` fails: ``` gen.h:17:12: error: expected identifier before numeric constant 17 | enum class 3 : int { a, b }; | ^ ``` The same defect affects generated **struct** names. For example: ```json {"type":"object","additionalProperties":false,"properties":{"2c":{"type":"array","items":{"type":"array","items":{"type":"object","additionalProperties":false,"properties":{"x":{"type":"string"}}}}}}} ``` emits: ```cpp struct 2cItemItem; // invalid: starts with a digit struct 2cItemItem { ... }; enum class Kind : uint8_t { Root1, 2cItemItem, Arr0, Arr1 }; // invalid enumerator std::optional<std::vector<std::vector<2cItemItem>>> _2c; // invalid ``` which also fails to compile: ``` gen.h:18:8: error: expected identifier before numeric constant 18 | struct 2cItemItem; ``` ## Expected vs actual - **Expected:** schemagen should produce a compilable header for any valid JSON Schema whose property/`$defs` names start with a digit (e.g. by prefixing the generated type name with `_` when it begins with a digit, the way `sanitize()` already does for member names). - **Actual:** schemagen exits 0 but emits type names beginning with a digit, and the generated header does not compile. ## Impact Any schema using a digit-leading property key with an enum value, or a digit-leading key wrapping arrays-of-arrays-of-objects (and similar hint derivations), yields non-compiling generated code. Digit-leading keys are common and valid (e.g. `"123"`, `"2024"`, `"2c"`), so this is reachable with ordinary schemas, not only adversarial ones. ## Files / lines - `contrib/schemagen/weaseljson_schemagen.py:153` — `camel()` returns digit-leading names. - `contrib/schemagen/weaseljson_schemagen.py:251` — `unique_name()` does not sanitize a leading digit. - `contrib/schemagen/weaseljson_schemagen.py:111-119` — `sanitize()` shows the existing correct handling for member names (`if s[0].isdigit(): s = "_" + s`), which is not applied to generated type names.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/weaseljson#59