Skip to content

fix: treat not as composition in no-required-schema-properties-undefined - #3108

Open
cpruijsen wants to merge 4 commits into
Redocly:mainfrom
cpruijsen:fix/issue-3104
Open

cpruijsen wants to merge 4 commits into
Redocly:mainfrom
cpruijsen:fix/issue-3104

Conversation

@cpruijsen

@cpruijsen cpruijsen commented Sep 11, 2026 •

Copy link
Copy Markdown

What/Why/How?

findCompositionRoot now walks not the same way it already walks allOf / anyOf / oneOf, so no-required-schema-properties-undefined checks required names inside not against the enclosing object (the usual JSON Schema form for mutual exclusion / absence). findCompositionRoot did not treat not as the same-instance relationship, so the (usually empty) not subschema was checked alone.

Typos inside not that do not exist on the enclosing schema still report.

Decision: walk not like the other composition keywords (option (b) on #3104).
Alternative: skip every required under not (option (a)).
Why: walking not matches the existing isCompositionChild helper and still catches undeclared names; can switch to (a), or also walk if / then / else.

Reference

Fixes #3104

Testing

  • Reproduced the reporter fixture at HEAD (two warnings on Contact.not.required).
  • Unit tests: mutual exclusion via not is silent; an extra undeclared name inside not still reports.
  • The new tests fail without the source change and pass with it.

Screenshots (optional)

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

Note

Low Risk
Targeted lint-rule behavior change with broad test coverage; no runtime API or security impact.

Overview
Fixes false positives in no-required-schema-properties-undefined when schemas use not: { required: [...] } for mutual exclusion (e.g. “must not require both email and phone”).

The rule now skips required arrays that sit lexically under a not keyword, because those names describe absence constraints, not missing properties declarations. The same skip applies on composed $ref paths when the ref (or its siblings) is directly under not; named schemas still get their own required checked when visited elsewhere, including when referenced only from a not exclusion.

Docs and unit tests cover not.required with declared/undeclared names, $ref siblings, nested composition, and the case where a bad required on a referenced schema is still reported.

Reviewed by Cursor Bugbot for commit 72d56c7. Bugbot is set up for automated code reviews on this repo. Configure here.

@cpruijsen
cpruijsen requested review from a team as code owners September 11, 2026 08:47
@changeset-bot

changeset-bot Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 72d56c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@redocly/openapi-core Patch
@redocly/cli Patch
@redocly/client-generator Patch
@redocly/respect-core Patch
@redocly/reunite-integration Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/rules/common/no-required-schema-properties-undefined.ts Outdated
@cpruijsen

Copy link
Copy Markdown
Author

You're right that walking not as composition was the wrong model. required under not is an absence assertion, so matching those names against properties is wrong in both directions: it flags not: { required: [missing] }, which is a valid restriction on an open object, and it stays quiet for not: { required: [email] } only because email happens to be declared. The rule now skips the check for the whole not subtree, so neither case reports.

Worth flagging one consequence of skipping the subtree rather than trying to classify: a nested object inside not that genuinely declares an undefined required name, for example not: { properties: { foo: { required: [bar] } } }, is now also skipped. That follows from the instruction to skip rather than to tell assertion-only required apart from nested shape, and I think it is the right trade, but say the word if you would rather it be narrower.

if / then / else still use the existing composition-root walk and are unchanged.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/rules/common/no-required-schema-properties-undefined.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/rules/common/no-required-schema-properties-undefined.ts Outdated
@cpruijsen
cpruijsen force-pushed the fix/issue-3104 branch 2 times, most recently from 04deb9f to 62fe88d Compare September 18, 2026 19:23

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

if (parent.not === chain[i]) return true;
}
return false;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isUnderNot misses $ref sibling not

Medium Severity

isUnderNot walks the resolved parents stack, so a not written next to $ref is invisible: Schema enter receives the resolved target, which does not own that not. The same gap hits the ref visitor after Schema leave. Valid OAS 3.1 $ref + sibling not forms still report required names inside not.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 62fe88d. Configure here.

@cpruijsen

Copy link
Copy Markdown
Author

Bugbot has a real one here, and it predates this PR.

A not written as a $ref sibling still reports. On 62fe88d:

Restricted:
  $ref: '#/components/schemas/Contact'
  not:
    required:
      - missing

gives Required property 'missing' is not defined. at #/components/schemas/Restricted/not/required/0. The same shape with required: [email], where Contact does declare email, reports as well, so that one is wrong however you read the not.

The cause is what Bugbot describes: the ref node owns the not, Schema enter receives the resolved target, so the node holding the not never reaches parents and the lexical check cannot see it. Bugbot flags the same gap on the ref leave path, which reads right to me, since it builds its chain from the same stack.

I ran those shapes against d1a2e42 as well and they report identically there, so this is not a regression from this PR. The case this PR does change is a plain not.required with no $ref involved: it reports on the base and is silent here. The tests I added cover not containing a $ref with a sibling required, and nobody covered not sitting next to one.

Closing the gap means deciding containment from the reported location's pointer rather than from the resolved parents stack, and that has to exclude a property literally named not under properties. It is a wider change than the one here, so I would rather you pick: fold it into this PR, or keep this PR at its current scope and track the $ref sibling case separately. Either suits me. Say which and I will push it.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a4f0fd9. Configure here.

if (parent.not === chain[i]) return true;
}
return false;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested not refs skip named schemas

Medium Severity

isUnderNot treats every descendant of a lexical not as skipped. A named schema first reached through a nested $ref (for example not.allOf) is skipped and marked seen, so its own required is never checked. The identity guard only covers a $ref that is the direct value of not.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a4f0fd9. Configure here.

cpruijsen and others added 4 commits September 23, 2026 11:04
Co-authored-by: Cursor <cursoragent@cursor.com>
The not check ran only in the Schema visitor, so a required sibling of a
$ref under not was still validated by the ref visitor. Both visitors now
share one isUnderNot helper, which also covers a $ref nested deeper in a
composition under not.
isUnderNot also matched the resolved target of not: { $ref }, so a named
schema reached first through such a ref had its own required list skipped and,
when nothing else referenced it, never validated at all. Whether the finding
appeared then depended on walk order. Match only an ancestor's own not value;
the four cases the skip exists for are all lexical.
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.

False positive no-required-schema-properties-undefined when required used inside not

3 participants