contrib/schemagen/weaseljson_schemagen.py copies both root containers of
named definitions ($defs and definitions) into a single flat dict and then
looks definitions up only by their last path segment. The container named
in the $ref is validated and then thrown away, so #/$defs/X and #/definitions/X are treated as the same definition.
That is not what JSON Schema says: $defs and definitions are two
independent locations. When both containers exist and contain an entry with
the same name, one silently shadows the other, and the generator emits the wrong C++ type / wrong validation for the property. The reverse case is
equally wrong in the other direction: a $ref to a definition that does not
exist in the container it names is silently accepted instead of being reported
as an unknown $ref target.
schemagen exits 0 with no diagnostic in every case below; the mistake is only
visible in the generated header's types and in the parser's accept/reject
behaviour.
python3 contrib/schemagen/weaseljson_schemagen.py schema.json -o gen.h --namespace ns
echo$?# 0, no diagnostic
sed -n '/^struct Root1 {/,/^};/p' gen.h
Actual (the definitions entry is used, not the $defs one that the $ref
names):
structRoot1{int64_ta=0;// "a"
};
Expected: std::optional<std::string> a; ($defs/X is {"type":"string"}).
Because the member is an int64_t, the generated parser now enforces the wrong
schema (RootBuilder::cbNumberData / cbStringData in the generated builder):
$printf'{"a":"hi"}'| ./demo # "$defs/X" says string -> should be OK2 # WeaselJson_REJECT (schema-valid document rejected)
$printf'{"a":1}'| ./demo # integer is not allowed by "$defs/X"0 # WeaselJson_OK (schema-invalid document accepted)
A document that is valid against the schema is rejected, and one that is
invalid is accepted — with no warning at generation time.
Reproduction 2 — an unresolvable $ref is silently accepted
schema2.json (Z exists only under definitions, but the property points at $defs):
The same silent aliasing happens for definitions-only schemas referenced
through #/$defs/...; $defs-only schemas referenced through #/definitions/... resolve too.
Impact
The generated header has the wrong member type, so downstream code silently
gets a value of the wrong type (or does not compile, if the two definitions
map to unrelated structs/enums).
The generated parser validates documents against a different definition than
the one the schema names — schema-valid documents are rejected
(WeaselJson_REJECT) and schema-invalid ones accepted (WeaselJson_OK),
which defeats the point of generating a validating parser.
A $ref typo/dangling reference (#/definitions/... left over from a
draft-07 schema while the definition lives in $defs) is not diagnosed even
though schemagen otherwise rejects unknown $ref targets.
Only schemas that keep definitions in both containers (e.g. a schema migrated
from draft-07 to 2020-12 that gained a $defs) are affected, which is
exactly when the aliasing is hardest to notice.
Suggested fix
Keep the two containers separate (or key everything by (container, name)): have ref_name() return the container together with the
name, and use it for both the lookup in build_def() and the self._building
cycle cache, so #/$defs/X can only ever resolve $defs/X and #/definitions/X can only resolve definitions/X. A $ref to a name that is
absent from the container it names should then fall through to the existing $ref to unknown defGenError.
Test gap
contrib/schemagen/test_schemagen.py (and the *.schema.json files) only ever
use one of the two containers, so nothing exercises a schema that has both, or
a $ref that names the container the definition is not in.
## Summary
`contrib/schemagen/weaseljson_schemagen.py` copies **both** root containers of
named definitions (`$defs` and `definitions`) into a single flat dict and then
looks definitions up **only by their last path segment**. The container named
in the `$ref` is validated and then thrown away, so `#/$defs/X` and
`#/definitions/X` are treated as the same definition.
That is not what JSON Schema says: `$defs` and `definitions` are two
independent locations. When both containers exist and contain an entry with
the same name, one silently shadows the other, and the generator emits the
**wrong C++ type / wrong validation** for the property. The reverse case is
equally wrong in the other direction: a `$ref` to a definition that does not
exist in the container it names is silently accepted instead of being reported
as an unknown `$ref` target.
schemagen exits 0 with no diagnostic in every case below; the mistake is only
visible in the generated header's types and in the parser's accept/reject
behaviour.
## Root cause
`contrib/schemagen/weaseljson_schemagen.py`, `Builder.__init__` (lines 241-245):
```python
class Builder:
def __init__(self, root_schema):
self.root_schema = root_schema
self.defs = {}
for key in ("$defs", "definitions"):
self.defs.update(root_schema.get(key, {})) # <-- merged, `definitions` wins
```
`Builder.ref_name()` (lines 293-299) parses the container out of the pointer but
returns only the last segment:
```python
def ref_name(self, ref):
...
parts = ref[2:].split("/")
if len(parts) != 2 or parts[0] not in ("$defs", "definitions"):
raise GenError(f"unsupported $ref target: {ref}")
return parts[1] # <-- container is dropped
```
and `Builder.build_def()` (lines 301-308) resolves the bare name against the
merged dict:
```python
if defname not in self.defs:
raise GenError(f"$ref to unknown def: {defname}")
node = self.defs[defname]
```
`self._building` (the `$ref`-cycle cache, line 248) is keyed the same way, so it
has the same aliasing problem.
## Reproduction 1 — the generated type is the *other* container's type
`schema.json`:
```json
{
"$defs": { "X": { "type": "string" } },
"definitions": { "X": { "type": "integer" } },
"type": "object",
"additionalProperties": false,
"required": ["a"],
"properties": { "a": { "$ref": "#/$defs/X" } }
}
```
```sh
python3 contrib/schemagen/weaseljson_schemagen.py schema.json -o gen.h --namespace ns
echo $? # 0, no diagnostic
sed -n '/^struct Root1 {/,/^};/p' gen.h
```
Actual (the `definitions` entry is used, not the `$defs` one that the `$ref`
names):
```cpp
struct Root1 {
int64_t a = 0; // "a"
};
```
Expected: `std::optional<std::string> a;` (`$defs/X` is `{"type":"string"}`).
Because the member is an `int64_t`, the generated parser now enforces the wrong
schema (`RootBuilder::cbNumberData` / `cbStringData` in the generated builder):
```console
$ printf '{"a":"hi"}' | ./demo # "$defs/X" says string -> should be OK
2 # WeaselJson_REJECT (schema-valid document rejected)
$ printf '{"a":1}' | ./demo # integer is not allowed by "$defs/X"
0 # WeaselJson_OK (schema-invalid document accepted)
```
A document that is valid against the schema is rejected, and one that is
invalid is accepted — with no warning at generation time.
## Reproduction 2 — an unresolvable `$ref` is silently accepted
`schema2.json` (`Z` exists only under `definitions`, but the property points at
`$defs`):
```json
{
"definitions": { "Z": { "type": "integer" } },
"type": "object",
"additionalProperties": false,
"required": ["b"],
"properties": { "b": { "$ref": "#/$defs/Z" } }
}
```
```console
$ python3 contrib/schemagen/weaseljson_schemagen.py schema2.json -o gen2.h --namespace ns
$ echo $?
0
$ sed -n '/^struct Root1 {/,/^};/p' gen2.h
struct Root1 {
int64_t b = 0; // "b"
};
```
`#/$defs/Z` does not exist, so this should be rejected the same way a genuinely
missing name is:
```console
$ printf '%s' '{"type":"object","additionalProperties":false,"properties":{"c":{"$ref":"#/$defs/Nope"}}}' > schema3.json
$ python3 contrib/schemagen/weaseljson_schemagen.py schema3.json -o gen3.h
error: $ref to unknown def: Nope
$ echo $?
1
```
The same silent aliasing happens for `definitions`-only schemas referenced
through `#/$defs/...`; `$defs`-only schemas referenced through
`#/definitions/...` resolve too.
## Impact
* The generated header has the wrong member type, so downstream code silently
gets a value of the wrong type (or does not compile, if the two definitions
map to unrelated structs/enums).
* The generated parser validates documents against a different definition than
the one the schema names — schema-valid documents are rejected
(`WeaselJson_REJECT`) and schema-invalid ones accepted (`WeaselJson_OK`),
which defeats the point of generating a validating parser.
* A `$ref` typo/dangling reference (`#/definitions/...` left over from a
draft-07 schema while the definition lives in `$defs`) is not diagnosed even
though `schemagen` otherwise rejects unknown `$ref` targets.
* Only schemas that keep definitions in both containers (e.g. a schema migrated
from draft-07 to 2020-12 that gained a `$defs`) are affected, which is
exactly when the aliasing is hardest to notice.
## Suggested fix
Keep the two containers separate (or key everything by
`(container, name)`): have `ref_name()` return the container together with the
name, and use it for both the lookup in `build_def()` and the `self._building`
cycle cache, so `#/$defs/X` can only ever resolve `$defs/X` and
`#/definitions/X` can only resolve `definitions/X`. A `$ref` to a name that is
absent from the container it names should then fall through to the existing
`$ref to unknown def` `GenError`.
## Test gap
`contrib/schemagen/test_schemagen.py` (and the `*.schema.json` files) only ever
use one of the two containers, so nothing exercises a schema that has both, or
a `$ref` that names the container the definition is not in.
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
contrib/schemagen/weaseljson_schemagen.pycopies both root containers ofnamed definitions (
$defsanddefinitions) into a single flat dict and thenlooks definitions up only by their last path segment. The container named
in the
$refis validated and then thrown away, so#/$defs/Xand#/definitions/Xare treated as the same definition.That is not what JSON Schema says:
$defsanddefinitionsare twoindependent locations. When both containers exist and contain an entry with
the same name, one silently shadows the other, and the generator emits the
wrong C++ type / wrong validation for the property. The reverse case is
equally wrong in the other direction: a
$refto a definition that does notexist in the container it names is silently accepted instead of being reported
as an unknown
$reftarget.schemagen exits 0 with no diagnostic in every case below; the mistake is only
visible in the generated header's types and in the parser's accept/reject
behaviour.
Root cause
contrib/schemagen/weaseljson_schemagen.py,Builder.__init__(lines 241-245):Builder.ref_name()(lines 293-299) parses the container out of the pointer butreturns only the last segment:
and
Builder.build_def()(lines 301-308) resolves the bare name against themerged dict:
self._building(the$ref-cycle cache, line 248) is keyed the same way, so ithas the same aliasing problem.
Reproduction 1 — the generated type is the other container's type
schema.json:Actual (the
definitionsentry is used, not the$defsone that the$refnames):
Expected:
std::optional<std::string> a;($defs/Xis{"type":"string"}).Because the member is an
int64_t, the generated parser now enforces the wrongschema (
RootBuilder::cbNumberData/cbStringDatain the generated builder):A document that is valid against the schema is rejected, and one that is
invalid is accepted — with no warning at generation time.
Reproduction 2 — an unresolvable
$refis silently acceptedschema2.json(Zexists only underdefinitions, but the property points at$defs):#/$defs/Zdoes not exist, so this should be rejected the same way a genuinelymissing name is:
The same silent aliasing happens for
definitions-only schemas referencedthrough
#/$defs/...;$defs-only schemas referenced through#/definitions/...resolve too.Impact
gets a value of the wrong type (or does not compile, if the two definitions
map to unrelated structs/enums).
the one the schema names — schema-valid documents are rejected
(
WeaselJson_REJECT) and schema-invalid ones accepted (WeaselJson_OK),which defeats the point of generating a validating parser.
$reftypo/dangling reference (#/definitions/...left over from adraft-07 schema while the definition lives in
$defs) is not diagnosed eventhough
schemagenotherwise rejects unknown$reftargets.from draft-07 to 2020-12 that gained a
$defs) are affected, which isexactly when the aliasing is hardest to notice.
Suggested fix
Keep the two containers separate (or key everything by
(container, name)): haveref_name()return the container together with thename, and use it for both the lookup in
build_def()and theself._buildingcycle cache, so
#/$defs/Xcan only ever resolve$defs/Xand#/definitions/Xcan only resolvedefinitions/X. A$refto a name that isabsent from the container it names should then fall through to the existing
$ref to unknown defGenError.Test gap
contrib/schemagen/test_schemagen.py(and the*.schema.jsonfiles) only everuse one of the two containers, so nothing exercises a schema that has both, or
a
$refthat names the container the definition is not in.