Skip to content

fix: do not report discriminator mapping schema names as unresolved refs - #3256

Open
HawkForge wants to merge 3 commits into
Redocly:mainfrom
HawkForge:fix/discriminator-mapping-schema-names
Open

HawkForge wants to merge 3 commits into
Redocly:mainfrom
HawkForge:fix/discriminator-mapping-schema-names

Conversation

@HawkForge

@HawkForge HawkForge commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What/Why/How?

The no-unresolved-refs rule resolved every discriminator mapping value as a $ref. A mapping value can also be a schema name (dog: Dog), and the OpenAPI spec recommends treating such a value as a schema name. The rule tried to resolve Dog as a relative file, failed, and reported Can't resolve $ref for each name.

The DiscriminatorMapping visitor now works the same way as spec-discriminator-defaultMapping does for defaultMapping:

  • A value that passes isMappingRef() is left to the ref visitor. The walker already turns mapping refs into $ref (directResolveAs), so the ref visitor reports the unresolvable ones. The visitor's own resolve() only added a duplicate report for a broken ref, and its return stopped checking after the first entry that resolved.
  • Any other value is a schema name and must match a key in components.schemas. Otherwise the rule reports Discriminator mapping value 'Doge' does not point to an existing schema component., so a typo is still caught.

The rule docs now mention the mapping checks.

Reference

Fixes #1877. Thanks @tobiaspatton-s4 for the report, and @jeremyfiel for narrowing it down to this rule.

Testing

Two tests in packages/core/src/rules/common/__tests__/no-unresolved-refs.test.ts:

  • A mapping of existing schema names (cat: Cat, dog: Dog) reports nothing. Without the fix it reports two Can't resolve $ref errors.
  • A mapping with a good ref, a typo name (dog: Doge) and a broken ref (bird: '#/components/schemas/Bird') reports exactly one problem for each bad entry. Without the fix only the broken ref is reported (by the ref visitor). The typo name is missed because the visitor stopped at the first entry that resolved.

npm run typecheck and npm run lint pass. The packages/core unit tests pass (149 files, 1187 tests). No fixture under packages, resources, tests or cookbook has a bare mapping name that the new check would report.

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
Narrow lint-rule change aligned with existing ref-utils behavior; only reduces false positives for valid discriminator mappings.

Overview
Fixes false positives from no-unresolved-refs when OpenAPI discriminator mapping values are schema names (e.g. dog: Dog) instead of $ref strings.

The DiscriminatorMapping visitor now uses isMappingRef() before resolving mapping values—the same rule the bundler already applies—so bare names are not treated as unresolved references. Actual ref values are still validated and reported when they cannot be resolved.

A regression test covers a Pet schema with mapping: { cat: Cat, dog: Dog } and expects zero lint errors.

Reviewed by Cursor Bugbot for commit 7f95b5c. Configure here.

@HawkForge
HawkForge requested review from a team as code owners October 15, 2026 11:47
@changeset-bot

changeset-bot Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 78b4918

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

This PR includes changesets to release 6 packages
Name Type
@redocly/openapi-core Patch
@redocly/cli Patch
@redocly/client-generator Patch
@redocly/recheck 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

) {
for (const mappingName of Object.keys(mapping)) {
// values that are not refs are schema names, the same way the bundler treats them
if (!isMappingRef(mapping[mappingName])) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about turning this around instead of skipping non-ref values? spec-discriminator-defaultMapping already does it for defaultMapping: a value is either a ref or a schema name, so a bare value gets checked against components.schemas. Otherwise a typo like dog: Doge passes silently after this change.

Something like:

let componentsSchemaNames: string[] = [];

return {
  Root: {
    enter(root) {
      componentsSchemaNames = Object.keys(root.components?.schemas || {});
    },
  },
  ref: { /* unchanged */ },
  DiscriminatorMapping(mapping: Record<string, string>, { report, location }: UserContext) {
    for (const mappingName of Object.keys(mapping)) {
      const mappingValue = mapping[mappingName];
      if (isMappingRef(mappingValue) || componentsSchemaNames.includes(mappingValue)) continue;

      report({
        message: `Discriminator mapping value '${mappingValue}' does not point to an existing schema component.`,
        location: location.child(mappingName),
        reference: 'https://redocly.com/docs/cli/rules/oas/no-unresolved-refs',
      });
    }
  },
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good call, turned it around like your sketch. bare values are now checked against components.schemas the same way spec-discriminator-defaultMapping does, so dog: Doge gets reported again.

refs are skipped in the mapping visitor like in your snippet. the walker already turns mapping refs into $ref (directResolveAs), so the ref visitor reports broken ones. the old resolve() there only added a duplicate report, and its return stopped after the first good entry anyway.

added a test with a good ref, a typo name and a broken ref, and it gets exactly one report for each bad one. updated the changeset too.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
latest ▓░░░░░░░░░░ 1.00x ▓░░░░░░░░░░ 1.00x ▓░░░░░░░░░░ 1.01x ± 0.04
next ▓░░░░░░░░░░ 1.01x ± 0.01 ▓░░░░░░░░░░ 1.01x ± 0.01 ▓░░░░░░░░░░ 1.00x

@tatomyr

tatomyr commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Could you update the PR description? It still says that the rule skips the mapping values that are not refs, and that the new test reports nothing. Also, please tick the required checklist items, so check-pr-required-checklist passes.

@HawkForge

Copy link
Copy Markdown
Contributor Author

updated the description, it now describes the schema name check and both tests. ticked the required boxes. also added a short note about the mapping checks to the rule docs.

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.

Bundler and linter fail when discriminator value is component name

3 participants