Skip to content

Ignore reference siblings during OAS 3.0 schema checks - #306

Draft
p1c2u wants to merge 3 commits into
masterfrom
fix/oas30-ref-schema-check
Draft

p1c2u wants to merge 3 commits into
masterfrom
fix/oas30-ref-schema-check

Conversation

@p1c2u

@p1c2u p1c2u commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Ignore reference siblings during OAS 3.0 schema checks

In OpenAPI 3.0 an object with $ref is a Reference Object, and "any properties added SHALL be ignored" (OAS 3.0.3, Reference Object). The OAS 3.0 validators already ignore these siblings during instance validation (ignore_ref_siblings). Two places still applied them:

  • Schema checks used the Draft 4 metaschema, which validates keys next to $ref. Valid documents such as {"$ref": "#/definitions/x", "description": 5} were rejected with SchemaError, including through validate().
  • evolve() picks a validator class from $schema. A $schema next to a $ref switched that part of the schema to another dialect and dropped OpenAPI semantics such as nullable.

Changes

  1. Reference-aware schema check. The OAS 3.0 validators (OAS30Validator, OAS30StrictValidator, OAS30ReadValidator, OAS30WriteValidator) check schemas against a private wrapper of the Draft 4 metaschema. Objects with $ref only need a string $ref; every other object is checked as before. Strict/Read/Write keep their previous Draft 4 format checker defaults.

  2. evolve() keeps the OAS 3.0 class when $ref and $schema appear together, also for enforce_properties_required=True. An id next to $ref no longer counts as a scope id either.

  3. Local reference targets are checked. Once siblings are ignored, schemas reachable only through a local reference (next to a root $ref, or under a non-schema key such as components) would no longer be checked. check_schema now follows reachable # / #/... JSON pointer references from the root and runs the same check on any target the structural pass did not cover. Each target is checked once, and recursive references terminate.

    The following are left to instance validation, because they depend on runtime resolution context:

    • external and anchor references
    • references inside a subschema with its own id
    • targets that declare another dialect via $schema
    • validate() calls with a legacy resolver or a root $schema

    check_schema(..., check_references=False) turns the target check off.

  4. evolve() overhead reduced. The wrapper from (2) runs on every descend, so the common dict case skips the Mapping ABC check.

  5. Docs describe the OAS 3.0 $ref behaviour in docs/validation.rst.

Behaviour changes

Case Before After
Malformed key next to $ref, e.g. {"$ref": ..., "minLength": "x"} SchemaError accepted
$schema next to $ref switches dialect ignored
Malformed schema reached through a local $ref under a non-schema key accepted SchemaError
Malformed schema under definitions that nothing references SchemaError SchemaError (unchanged)

The third row makes checks stricter: a document that passes today can now raise SchemaError if a local $ref points at a malformed schema. Release notes should mention it.

OAS 3.1 and 3.2 validators are unchanged and still apply $ref siblings, as JSON Schema 2020-12 requires.

Performance

Best of 5 runs, same machine, Python 3.14, jsonschema 4.26.0:

Benchmark master this PR
Recursive refs, 20 validations 770 ms 820 ms
50 nested properties, no refs, 500 validations 170 ms 190 ms
check_schema, 300 cross-referencing definitions 22 ms 30 ms

The remaining validation overhead comes from the evolve() wrapper in (2).

Not in scope

  • Malformed id values inside ignored siblings can still break referencing's registry crawl when an unresolved URI reference triggers discovery. This already happens on master and is better fixed in its own PR.
  • OpenAPI 3.0 resolves references as JSON Reference, not JSON Schema (OAS 3.0.3), so id scoping arguably should not apply at all. Dropping it would break existing users and needs a deprecation cycle first.

Testing

  • pytest: 325 passed, 9 skipped (master: 277 passed, 6 skipped). Also passes with the minimum supported jsonschema==4.19.1, apart from the 3 discriminator tests that already fail on master with that version.
  • New tests/unit/test_oas30_reference_targets.py: 31 tests covering transitive and escaped pointers, recursion, unresolvable and non-pointer refs, id scopes, foreign dialects, legacy resolvers, enforce_properties_required, and dedup of already-covered targets. _references.py has 97% coverage.
  • black, isort, flynt, pyupgrade --py36-plus, deptry and the Sphinx docs build (-n -W) pass. mypy reports no new errors; the 2 errors in _regex.py already exist on master.

p1c2u added 3 commits November 9, 2026 15:29
Reference siblings are ignored by the OAS 3.0 schema check, so schemas reachable only through a local reference (next to a root $ref, or under a non-schema key such as components) were no longer checked. Follow reachable JSON pointer references from the root and check targets the structural check did not cover.

References that depend on runtime resolution context are left to instance validation: external and anchor references, references inside a subschema with its own id, targets declaring another dialect, and shortcut calls with a legacy resolver or a root $schema.
evolve runs on every descend; avoid the Mapping ABC check for plain dicts and test the rarer $schema key first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant