schemagen: a property/$defs name that camel-cases to RootBuilder collides with the generated builder class (generated header does not compile) #69

Open
opened 2026-10-18 14:31:28 +00:00 by weaselbot · 0 comments
Member

Summary

contrib/schemagen/weaseljson_schemagen.py writes several identifiers of its own into the generated header, but Builder._is_reserved() only reserves two of them (Root, RootScalar). A perfectly valid schema whose CamelCase type name is RootBuilder therefore gets a namespace-scope struct RootBuilder, which collides with the builder class the generator itself emits at namespace scope:

struct RootBuilder { ... };   // user type (from the schema)
class RootBuilder { ... };    // generated builder  -> redefinition

schemagen exits 0 with no diagnostic; the breakage only shows up when the generated header is compiled.

Root cause

contrib/schemagen/weaseljson_schemagen.py, Builder._is_reserved() (lines 270-274):

    @staticmethod
    def _is_reserved(name):
        \"\"\"Names generated internally that must not collide with user types.\"\"\"
        return name in (\"Root\", \"RootScalar\")

unique_name()/_name_taken() (lines 251-268) is the only guard against collisions, so any user object/enum name equal to one of the identifiers the emitter writes is accepted. The identifiers the emitter actually writes are:

  • namespace scope: Root alias (_root_alias), RootInner (Emitter.emit), and RootBuilder (the class in _builder()'s f-string, line 971) - only Root is reserved;
  • inside RootBuilder: Kind (line 726/1005), Cat (line 1006), SlotInfo (line 1007), Frame (line 1014) - none reserved.

Reproduction 1 - property key

repro.json:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "root_builder": {
      "type": "object",
      "additionalProperties": false,
      "properties": { "x": { "type": "integer" } }
    }
  }
}
$ python3 contrib/schemagen/weaseljson_schemagen.py repro.json -o gen.h --namespace repro
$ echo $?
0
$ printf '#include "gen.h"\nint main(){ repro::Root r{}; (void)r; }\n' > use.cpp
$ clang++ -std=c++20 -I include -I . -fsyntax-only use.cpp

Generated code (gen.h lines 16-31):

struct Root1;
struct RootBuilder;

struct RootBuilder {
  std::optional<int64_t> x;  // "x"
};

struct Root1 {
  std::optional<RootBuilder> root_builder;  // "root_builder"
};

using Root = Root1;

class RootBuilder {
public:
  explicit RootBuilder(int stackSize = 1024) {

Error:

gen.h:31:7: error: redefinition of 'RootBuilder'
   31 | class RootBuilder {
gen.h:20:8: note: previous definition is here
   20 | struct RootBuilder {

"root_builder", "root-builder" and "RootBuilder" all camel-case to RootBuilder, and camel() is applied to $defs/definitions keys as well, so the same thing happens with:

Reproduction 2 - $defs entry

{
  "type": "object",
  "additionalProperties": false,
  "properties": { "a": { "$ref": "#/$defs/RootBuilder" } },
  "$defs": {
    "RootBuilder": {
      "type": "object",
      "additionalProperties": false,
      "properties": { "x": { "type": "integer" } }
    }
  }
}

Same error: redefinition of 'RootBuilder'. An enum-valued property named root_builder fails too (error: use of 'RootBuilder' with tag type that does not match previous declaration), so it is not specific to nested objects.

Reproduction 3 - the same gap shadows user types inside RootBuilder

Object-valued properties named kind, cat, frame, slot_info produce struct Kind / struct Cat / struct Frame / struct SlotInfo at namespace scope. Inside the builder the nested enum class Kind, enum class Cat, struct SlotInfo, struct Frame shadow them, so the generated engage() becomes ill-formed:

{"type":"object","additionalProperties":false,
 "properties":{"frame":{"type":"object","additionalProperties":false,
                        "properties":{"x":{"type":"integer"}}}}}
gen.h:414:25: error: no member named 'x' in 'repro::RootBuilder::Frame'
  414 |       case 0: { if (!o->x) o->x.emplace(); return &*o->x; }

and for kind/cat:

gen.h:414:23: error: member reference base type 'Kind' is not a structure or union
gen.h:414:23: error: member reference base type 'Cat' is not a structure or union

Impact

Any valid JSON Schema using one of these names for an object/enum-valued property (or for a $defs entry) produces a header that cannot be compiled. schemagen reports success, so the failure surfaces only in the consumer's build. kind and frame are ordinary JSON property names, and root_builder/RootBuilder is a plausible $defs name.

Why this is not a duplicate

  • #21 (closed) fixed this class of collision only for Root, Skip, RootScalar and duplicate ArrN/Kind enumerators; the builder class name RootBuilder is not mentioned there, is still missing from _is_reserved, and the regression tests added for #21 (test_root_alias_does_not_collide_with_user_type, test_kind_enum_does_not_duplicate_arr0, test_skip_user_type_is_allowed) do not cover it.
  • #68 is a member name colliding with the type name of its value (two separate namespaces in _build_object).
  • #59/#60 are type names that are not valid C++ identifiers.

This is a collision between a user type name and an identifier the generator itself emits at the same scope.

Suggested fix

Extend _is_reserved() with the emitted names (RootBuilder, RootInner, Kind, Cat, SlotInfo, Frame), or rename them to a reserved prefix, and add a compile-checked regression test (the existing SchemagenCollisionTest.generate_and_compile helper in test_schemagen.py can be reused).

## Summary `contrib/schemagen/weaseljson_schemagen.py` writes several identifiers of its own into the generated header, but `Builder._is_reserved()` only reserves two of them (`Root`, `RootScalar`). A perfectly valid schema whose CamelCase type name is `RootBuilder` therefore gets a namespace-scope `struct RootBuilder`, which collides with the builder class the generator itself emits at namespace scope: ```cpp struct RootBuilder { ... }; // user type (from the schema) class RootBuilder { ... }; // generated builder -> redefinition ``` schemagen exits 0 with no diagnostic; the breakage only shows up when the generated header is compiled. ## Root cause `contrib/schemagen/weaseljson_schemagen.py`, `Builder._is_reserved()` (lines 270-274): ```python @staticmethod def _is_reserved(name): \"\"\"Names generated internally that must not collide with user types.\"\"\" return name in (\"Root\", \"RootScalar\") ``` `unique_name()`/`_name_taken()` (lines 251-268) is the only guard against collisions, so any user object/enum name equal to one of the identifiers the emitter writes is accepted. The identifiers the emitter actually writes are: * namespace scope: `Root` alias (`_root_alias`), `RootInner` (`Emitter.emit`), and `RootBuilder` (the class in `_builder()`'s f-string, line 971) - only `Root` is reserved; * inside `RootBuilder`: `Kind` (line 726/1005), `Cat` (line 1006), `SlotInfo` (line 1007), `Frame` (line 1014) - none reserved. ## Reproduction 1 - property key `repro.json`: ```json { "type": "object", "additionalProperties": false, "properties": { "root_builder": { "type": "object", "additionalProperties": false, "properties": { "x": { "type": "integer" } } } } } ``` ```sh $ python3 contrib/schemagen/weaseljson_schemagen.py repro.json -o gen.h --namespace repro $ echo $? 0 $ printf '#include "gen.h"\nint main(){ repro::Root r{}; (void)r; }\n' > use.cpp $ clang++ -std=c++20 -I include -I . -fsyntax-only use.cpp ``` Generated code (gen.h lines 16-31): ```cpp struct Root1; struct RootBuilder; struct RootBuilder { std::optional<int64_t> x; // "x" }; struct Root1 { std::optional<RootBuilder> root_builder; // "root_builder" }; using Root = Root1; class RootBuilder { public: explicit RootBuilder(int stackSize = 1024) { ``` Error: ``` gen.h:31:7: error: redefinition of 'RootBuilder' 31 | class RootBuilder { gen.h:20:8: note: previous definition is here 20 | struct RootBuilder { ``` `"root_builder"`, `"root-builder"` and `"RootBuilder"` all camel-case to `RootBuilder`, and `camel()` is applied to `$defs`/`definitions` keys as well, so the same thing happens with: ## Reproduction 2 - `$defs` entry ```json { "type": "object", "additionalProperties": false, "properties": { "a": { "$ref": "#/$defs/RootBuilder" } }, "$defs": { "RootBuilder": { "type": "object", "additionalProperties": false, "properties": { "x": { "type": "integer" } } } } } ``` Same `error: redefinition of 'RootBuilder'`. An enum-valued property named `root_builder` fails too (`error: use of 'RootBuilder' with tag type that does not match previous declaration`), so it is not specific to nested objects. ## Reproduction 3 - the same gap shadows user types inside `RootBuilder` Object-valued properties named `kind`, `cat`, `frame`, `slot_info` produce `struct Kind` / `struct Cat` / `struct Frame` / `struct SlotInfo` at namespace scope. Inside the builder the nested `enum class Kind`, `enum class Cat`, `struct SlotInfo`, `struct Frame` shadow them, so the generated `engage()` becomes ill-formed: ```json {"type":"object","additionalProperties":false, "properties":{"frame":{"type":"object","additionalProperties":false, "properties":{"x":{"type":"integer"}}}}} ``` ``` gen.h:414:25: error: no member named 'x' in 'repro::RootBuilder::Frame' 414 | case 0: { if (!o->x) o->x.emplace(); return &*o->x; } ``` and for `kind`/`cat`: ``` gen.h:414:23: error: member reference base type 'Kind' is not a structure or union gen.h:414:23: error: member reference base type 'Cat' is not a structure or union ``` ## Impact Any valid JSON Schema using one of these names for an object/enum-valued property (or for a `$defs` entry) produces a header that cannot be compiled. schemagen reports success, so the failure surfaces only in the consumer's build. `kind` and `frame` are ordinary JSON property names, and `root_builder`/`RootBuilder` is a plausible `$defs` name. ## Why this is not a duplicate * #21 (closed) fixed this class of collision only for `Root`, `Skip`, `RootScalar` and duplicate `ArrN`/`Kind` **enumerators**; the builder **class name** `RootBuilder` is not mentioned there, is still missing from `_is_reserved`, and the regression tests added for #21 (`test_root_alias_does_not_collide_with_user_type`, `test_kind_enum_does_not_duplicate_arr0`, `test_skip_user_type_is_allowed`) do not cover it. * #68 is a *member* name colliding with the *type* name of its value (two separate namespaces in `_build_object`). * #59/#60 are type names that are not valid C++ identifiers. This is a collision between a user type name and an identifier the generator itself emits at the same scope. ## Suggested fix Extend `_is_reserved()` with the emitted names (`RootBuilder`, `RootInner`, `Kind`, `Cat`, `SlotInfo`, `Frame`), or rename them to a reserved prefix, and add a compile-checked regression test (the existing `SchemagenCollisionTest.generate_and_compile` helper in `test_schemagen.py` can be reused).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/weaseljson#69