When a $defs entry is nullable ("type": ["object", "null"]) and is referenced from a field that forms a cycle, the generated builder loses the nullable flag on the recursive field.
Relevant code in contrib/schemagen/weaseljson_schemagen.py:
Lines 224-232 (build_def): returns self.build_type(node, defname, defname=defname). For objects, build_type returns a bare TObj and stores only that bare type in self._building (see line 317-318 in _build_object).
Lines 312-338 (_build_object): caches TObj(name) in self._building[defname]before recursing into fields, so any recursive $ref sees the cached bare type and never gets the (TObj, nullable) tuple.
With this schema, the generated code accepts null at the top-level self field, but rejects null at the nested self field.
Example inputs and results:
\{"self": null\} finishes with OK (correct).
\{"self": {"self": null}\} returns REJECT during parsing (incorrect).
\{"self": {"self": {}}}\} returns OK, confirming the recursive field is treated as a non-nullable object.
Expected behavior: because Self is declared as ["object", "null"], both the outer self field and the inner self field should be nullable; \{"self":{"self":null}\} should be accepted.
Actual behavior: the first (non-recursive) reference receives the correct (TObj, nullable=True) result, but the recursive reference inside Self gets the cached bare TObj and is therefore non-nullable.
Impact: any self-referential schema that also allows null will reject valid documents at the recursive level. This makes the generator unusable for common tree/graph schemas where child pointers may be absent (e.g. linked lists, nullable child nodes).
When a `$defs` entry is nullable (`"type": ["object", "null"]`) and is referenced from a field that forms a cycle, the generated builder loses the nullable flag on the recursive field.
Relevant code in `contrib/schemagen/weaseljson_schemagen.py`:
- Lines 224-232 (`build_def`): returns `self.build_type(node, defname, defname=defname)`. For objects, `build_type` returns a bare `TObj` and stores only that bare type in `self._building` (see line 317-318 in `_build_object`).
- Lines 312-338 (`_build_object`): caches `TObj(name)` in `self._building[defname]` *before* recursing into fields, so any recursive `$ref` sees the cached bare type and never gets the `(TObj, nullable)` tuple.
Reproduction schema:
```json
{
"type": "object",
"properties": {
"self": { "$ref": "#/$defs/Self" }
},
"$defs": {
"Self": {
"type": ["object", "null"],
"properties": {
"self": { "$ref": "#/$defs/Self" }
}
}
}
}
```
With this schema, the generated code accepts `null` at the top-level `self` field, but rejects `null` at the nested `self` field.
Example inputs and results:
- `\{"self": null\}` finishes with `OK` (correct).
- `\{"self": {"self": null}\}` returns `REJECT` during parsing (incorrect).
- `\{"self": {"self": {}}}\}` returns `OK`, confirming the recursive field is treated as a non-nullable object.
Expected behavior: because `Self` is declared as `["object", "null"]`, *both* the outer `self` field and the inner `self` field should be nullable; `\{"self":{"self":null}\}` should be accepted.
Actual behavior: the first (non-recursive) reference receives the correct `(TObj, nullable=True)` result, but the recursive reference inside `Self` gets the cached bare `TObj` and is therefore non-nullable.
Impact: any self-referential schema that also allows `null` will reject valid documents at the recursive level. This makes the generator unusable for common tree/graph schemas where child pointers may be absent (e.g. linked lists, nullable child nodes).
weaselbot
was assigned by andrew2026-06-30 16:38:43 +00:00
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.
When a
$defsentry is nullable ("type": ["object", "null"]) and is referenced from a field that forms a cycle, the generated builder loses the nullable flag on the recursive field.Relevant code in
contrib/schemagen/weaseljson_schemagen.py:build_def): returnsself.build_type(node, defname, defname=defname). For objects,build_typereturns a bareTObjand stores only that bare type inself._building(see line 317-318 in_build_object)._build_object): cachesTObj(name)inself._building[defname]before recursing into fields, so any recursive$refsees the cached bare type and never gets the(TObj, nullable)tuple.Reproduction schema:
With this schema, the generated code accepts
nullat the top-levelselffield, but rejectsnullat the nestedselffield.Example inputs and results:
\{"self": null\}finishes withOK(correct).\{"self": {"self": null}\}returnsREJECTduring parsing (incorrect).\{"self": {"self": {}}}\}returnsOK, confirming the recursive field is treated as a non-nullable object.Expected behavior: because
Selfis declared as["object", "null"], both the outerselffield and the innerselffield should be nullable;\{"self":{"self":null}\}should be accepted.Actual behavior: the first (non-recursive) reference receives the correct
(TObj, nullable=True)result, but the recursive reference insideSelfgets the cached bareTObjand is therefore non-nullable.Impact: any self-referential schema that also allows
nullwill reject valid documents at the recursive level. This makes the generator unusable for common tree/graph schemas where child pointers may be absent (e.g. linked lists, nullable child nodes).