schemagen drops nullability on cyclic $ref targets #14

Closed
opened 2026-06-18 23:20:40 +00:00 by weaselbot · 0 comments
Member

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:

{
  "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).

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 andrew 2026-06-30 16:38:43 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/weaseljson#14