weaseljson_schemagen.py derives a struct member name from a property key with sanitize() (contrib/schemagen/weaseljson_schemagen.py:111) and a type name for
the property's value type from the same key with camel() via unique_name() (:153, :251). These two names are allocated from two
completely separate namespaces (seen_cpp per struct at :451-460 for members, self._used_names at :264/:587 for types), so nothing prevents them from
being identical.
Whenever a property key is "already CamelCase" (it starts with an uppercase letter
and contains only [A-Za-z0-9_], so sanitize(key) == camel(key)) and the
property's value produces a named type (an inline object, a $ref to an object,
or an enum), the generated struct contains a member with the same name as the
type it uses:
That is ill-formed C++ (the member declaration changes the meaning of the type
name in class scope). schemagen still exits 0 with no diagnostic, so the breakage
is only discovered when the generated header is compiled. With a second member
that uses the same type, Address then resolves to the member, which breaks
both GCC and Clang.
Capitalized property names ("Address", "User", "Id", "Name", ...) are
extremely common in real JSON, so this makes the generator unusable for those
schemas.
Root cause
Builder._build_object() (:412) names the struct via self.unique_name(defname or hint) (:413) and gives each field cpp = sanitize(key) (:454); the dedupe loop at :457-460 only compares
member names against other member names of the same struct.
Emitter._struct_decls() (:688) blindly emits f' {store} {f.cpp}...' (:705)
and never checks the member name against the generated type names, which live in self.b._used_names / _is_reserved() and are only consulted by unique_name().
Since sanitize("Address") == "Address" and camel("Address") == "Address", the
collision is guaranteed for capitalized keys.
gen.h:25:26: error: declaration of 'std::optional<demo::Address> demo::Root1::Address' changes meaning of 'Address' [-Wchanges-meaning]
25 | std::optional<Address> Address; // "Address"
| ^~~~~~~
gen.h:25:17: note: used here to mean 'struct demo::Address'
gen.h:20:8: note: declared here
20 | struct Address {
Expected: the header compiles (nothing in this schema is outside the documented
supported subset).
The same happens with an enum-valued capitalized property:
enum class Color : int { red, green };
...
gen.h:22:24: error: declaration of 'std::optional<demo::Color> demo::Root1::Color' changes meaning of 'Color' [-Wchanges-meaning]
Renaming the property to lowercase ("color") makes it compile, which shows the
trigger is purely the sanitize()/camel() name collision.
Reproduction 2 — $ref'd type used by two properties (GCC and Clang)
Here the second member uses the shadowed type name, so both compilers reject it:
$ g++ -std=c++20 -I include -c use.cpp -o /dev/null
gen.h:25:26: error: declaration of 'std::optional<demo::Address> demo::Root1::Address' changes meaning of 'Address' [-Wchanges-meaning]
gen.h:26:17: error: invalid use of non-static data member 'demo::Root1::Address'
gen.h:410:25: error: 'struct demo::Root1' has no member named 'home'
$ clang++ -std=c++20 -I include -c use.cpp -o /dev/null
gen.h:26:17: error: invalid use of non-static data member 'Address'
26 | std::optional<Address> home; // "home"
1 error generated.
Suggested fix
Allocate member names from the same registry as type names (e.g. check sanitize(key) against self.b._used_names/_name_taken() in _build_object() and suffix the member — or the type — until it no longer
collides), so a member never shadows a namespace-scope type it or a sibling
member uses.
Test gap
contrib/schemagen/test_schemagen.py has a generate_and_compile() helper, but
every schema it compiles uses lowercase property keys, so no existing test covers
a capitalized property key whose value has a generated type name.
Related, but distinct
#21 (closed) reserved the generated names Root/Skip/ArrN/RootScalar +
the Kind enum.
#59 / #60 (open) are about type names that are not valid identifiers
(digit-leading, ./+//), #62 (open) about member names that are keywords.
None of them covers a member name that is a valid identifier colliding with a
generated type name.
## Summary
`weaseljson_schemagen.py` derives a struct **member name** from a property key with
`sanitize()` (`contrib/schemagen/weaseljson_schemagen.py:111`) and a **type name** for
the property's value type from the *same* key with `camel()` via
`unique_name()` (`:153`, `:251`). These two names are allocated from two
completely separate namespaces (`seen_cpp` per struct at `:451-460` for members,
`self._used_names` at `:264`/`:587` for types), so nothing prevents them from
being **identical**.
Whenever a property key is "already CamelCase" (it starts with an uppercase letter
and contains only `[A-Za-z0-9_]`, so `sanitize(key) == camel(key)`) **and** the
property's value produces a named type (an inline object, a `$ref` to an object,
or an `enum`), the generated struct contains a member with the same name as the
type it uses:
```cpp
struct Root1 {
std::optional<Address> Address; // "Address"
};
```
That is ill-formed C++ (the member declaration changes the meaning of the type
name in class scope). schemagen still exits 0 with no diagnostic, so the breakage
is only discovered when the generated header is compiled. With a second member
that uses the same type, `Address` then resolves to the *member*, which breaks
both GCC and Clang.
Capitalized property names (`"Address"`, `"User"`, `"Id"`, `"Name"`, ...) are
extremely common in real JSON, so this makes the generator unusable for those
schemas.
## Root cause
- `Builder._build_object()` (`:412`) names the struct via
`self.unique_name(defname or hint)` (`:413`) and gives each field
`cpp = sanitize(key)` (`:454`); the dedupe loop at `:457-460` only compares
member names against *other member names* of the same struct.
- `Emitter._struct_decls()` (`:688`) blindly emits `f' {store} {f.cpp}...'` (`:705`)
and never checks the member name against the generated type names, which live in
`self.b._used_names` / `_is_reserved()` and are only consulted by `unique_name()`.
- Since `sanitize("Address") == "Address"` and `camel("Address") == "Address"`, the
collision is guaranteed for capitalized keys.
## Reproduction 1 — inline object property (GCC)
`schema.json`:
```json
{
"type": "object",
"additionalProperties": false,
"properties": {
"Address": {
"type": "object",
"additionalProperties": false,
"properties": { "city": { "type": "string" } }
}
}
}
```
```sh
python3 contrib/schemagen/weaseljson_schemagen.py schema.json -o gen.h --namespace demo
echo $? # 0, no diagnostic
printf '#include "gen.h"\nint main(){ demo::RootBuilder b; }\n' > use.cpp
g++ -std=c++20 -I include -c use.cpp -o /dev/null
```
Generated (`gen.h`):
```cpp
struct Address {
std::optional<std::string> city; // "city"
};
struct Root1 {
std::optional<Address> Address; // "Address"
};
using Root = Root1;
```
Actual (g++ 16.1.1):
```
gen.h:25:26: error: declaration of 'std::optional<demo::Address> demo::Root1::Address' changes meaning of 'Address' [-Wchanges-meaning]
25 | std::optional<Address> Address; // "Address"
| ^~~~~~~
gen.h:25:17: note: used here to mean 'struct demo::Address'
gen.h:20:8: note: declared here
20 | struct Address {
```
Expected: the header compiles (nothing in this schema is outside the documented
supported subset).
The same happens with an enum-valued capitalized property:
```json
{"type":"object","additionalProperties":false,"properties":{"Color":{"enum":["red","green"]}}}
```
```
enum class Color : int { red, green };
...
gen.h:22:24: error: declaration of 'std::optional<demo::Color> demo::Root1::Color' changes meaning of 'Color' [-Wchanges-meaning]
```
Renaming the property to lowercase (`"color"`) makes it compile, which shows the
trigger is purely the `sanitize()`/`camel()` name collision.
## Reproduction 2 — `$ref`'d type used by two properties (GCC **and** Clang)
`schema.json`:
```json
{
"$defs": {
"Address": {
"type": "object",
"additionalProperties": false,
"properties": { "city": { "type": "string" } }
}
},
"type": "object",
"additionalProperties": false,
"properties": {
"Address": { "$ref": "#/$defs/Address" },
"home": { "$ref": "#/$defs/Address" }
}
}
```
Generated:
```cpp
struct Root1 {
std::optional<Address> Address; // "Address"
std::optional<Address> home; // "home"
};
```
Here the *second* member uses the shadowed type name, so both compilers reject it:
```
$ g++ -std=c++20 -I include -c use.cpp -o /dev/null
gen.h:25:26: error: declaration of 'std::optional<demo::Address> demo::Root1::Address' changes meaning of 'Address' [-Wchanges-meaning]
gen.h:26:17: error: invalid use of non-static data member 'demo::Root1::Address'
gen.h:410:25: error: 'struct demo::Root1' has no member named 'home'
$ clang++ -std=c++20 -I include -c use.cpp -o /dev/null
gen.h:26:17: error: invalid use of non-static data member 'Address'
26 | std::optional<Address> home; // "home"
1 error generated.
```
## Suggested fix
Allocate member names from the same registry as type names (e.g. check
`sanitize(key)` against `self.b._used_names`/`_name_taken()` in
`_build_object()` and suffix the *member* — or the *type* — until it no longer
collides), so a member never shadows a namespace-scope type it or a sibling
member uses.
## Test gap
`contrib/schemagen/test_schemagen.py` has a `generate_and_compile()` helper, but
every schema it compiles uses lowercase property keys, so no existing test covers
a capitalized property key whose value has a generated type name.
## Related, but distinct
- #21 (closed) reserved the *generated* names `Root`/`Skip`/`ArrN`/`RootScalar` +
the `Kind` enum.
- #59 / #60 (open) are about type names that are not valid identifiers
(digit-leading, `.`/`+`/`/`), #62 (open) about member names that are keywords.
- None of them covers a member name that is a *valid identifier* colliding with a
generated type name.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
weaseljson_schemagen.pyderives a struct member name from a property key withsanitize()(contrib/schemagen/weaseljson_schemagen.py:111) and a type name forthe property's value type from the same key with
camel()viaunique_name()(:153,:251). These two names are allocated from twocompletely separate namespaces (
seen_cppper struct at:451-460for members,self._used_namesat:264/:587for types), so nothing prevents them frombeing identical.
Whenever a property key is "already CamelCase" (it starts with an uppercase letter
and contains only
[A-Za-z0-9_], sosanitize(key) == camel(key)) and theproperty's value produces a named type (an inline object, a
$refto an object,or an
enum), the generated struct contains a member with the same name as thetype it uses:
That is ill-formed C++ (the member declaration changes the meaning of the type
name in class scope). schemagen still exits 0 with no diagnostic, so the breakage
is only discovered when the generated header is compiled. With a second member
that uses the same type,
Addressthen resolves to the member, which breaksboth GCC and Clang.
Capitalized property names (
"Address","User","Id","Name", ...) areextremely common in real JSON, so this makes the generator unusable for those
schemas.
Root cause
Builder._build_object()(:412) names the struct viaself.unique_name(defname or hint)(:413) and gives each fieldcpp = sanitize(key)(:454); the dedupe loop at:457-460only comparesmember names against other member names of the same struct.
Emitter._struct_decls()(:688) blindly emitsf' {store} {f.cpp}...'(:705)and never checks the member name against the generated type names, which live in
self.b._used_names/_is_reserved()and are only consulted byunique_name().sanitize("Address") == "Address"andcamel("Address") == "Address", thecollision is guaranteed for capitalized keys.
Reproduction 1 — inline object property (GCC)
schema.json:Generated (
gen.h):Actual (g++ 16.1.1):
Expected: the header compiles (nothing in this schema is outside the documented
supported subset).
The same happens with an enum-valued capitalized property:
Renaming the property to lowercase (
"color") makes it compile, which shows thetrigger is purely the
sanitize()/camel()name collision.Reproduction 2 —
$ref'd type used by two properties (GCC and Clang)schema.json:Generated:
Here the second member uses the shadowed type name, so both compilers reject it:
Suggested fix
Allocate member names from the same registry as type names (e.g. check
sanitize(key)againstself.b._used_names/_name_taken()in_build_object()and suffix the member — or the type — until it no longercollides), so a member never shadows a namespace-scope type it or a sibling
member uses.
Test gap
contrib/schemagen/test_schemagen.pyhas agenerate_and_compile()helper, butevery schema it compiles uses lowercase property keys, so no existing test covers
a capitalized property key whose value has a generated type name.
Related, but distinct
Root/Skip/ArrN/RootScalar+the
Kindenum.(digit-leading,
./+//), #62 (open) about member names that are keywords.generated type name.