Skip to content

feat(validation): reject directive definition cycles#4726

Merged
yaacovCR merged 1 commit into
graphql:17.x.xfrom
yaacovCR:no-directive-definition-cycles-17
Jun 12, 2026
Merged

feat(validation): reject directive definition cycles#4726
yaacovCR merged 1 commit into
graphql:17.x.xfrom
yaacovCR:no-directive-definition-cycles-17

Conversation

@yaacovCR

@yaacovCR yaacovCR commented May 8, 2026

Copy link
Copy Markdown
Contributor

Adds a new NoDirectiveDefinitionCyclesRule SDL validation rule that detects cycles in the graph of directives used within directive definitions.

What is a directive definition cycle?

A cycle arises when following the references within directive definitions leads back to the same directive. Two kinds of references are tracked:

  1. Directive argument types — if @foo takes an argument of type InputObject, and InputObject has a field with directive @foo, that forms a cycle: @foo → InputObject → @foo.
  2. Directives on directive definitions — when the experimental experimentalDirectivesOnDirectiveDefinitions parser option is enabled, a directive applied directly to a directive definition or extension is also a reference. If @a is annotated with @b and @b is annotated with @a, that is a cycle: @a → @b → @a.

Cycles make it impossible to build a well-defined schema; the spec already forbids them, and experimental directive extensions do the same.

Key design question: should we consider existing schema applied directives?

In this initial attempt, when validating an SDL extension document against an existing schema, only references that appear in the document being validated are considered to close a cycle. Applied directives already persisted in the base schema are not counted as cycle-forming edges.

This means you can have a pre-existing schema like:

directive @a @b on DIRECTIVE_DEFINITION
directive @b on DIRECTIVE_DEFINITION

…where @a already carries @b in the stored schema, and then in your extension document write:

extend directive @b @a

This will not be reported as a cycle (@a → @b → @a), because the @b edge on @a comes from the original schema, not from the document under validation. The rule only errors when at least one edge in the detected cycle is introduced by the new document. This is because applied directives, while accessible via the stored definitions/extensions, do not actually form part of the existing schema object => at least, per the specification.

Is this the desired behavior? @BoD @graphql/tsc

@yaacovCR yaacovCR added spec RFC Implementation of a proposed change to the GraphQL specification PR: bug fix 🐞 requires increase of "patch" version number labels May 8, 2026
@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown

@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@BoD

BoD commented May 13, 2026

Copy link
Copy Markdown
Contributor

Thank you for looking into this and implementing the validation @yaacovCR!

About extensions: to me, and generally speaking, it would be surprising if a declaration that is invalid in the context of a schema could still be made valid via extensions. Unless this can already happen otherwise in the current spec, I would be in favor of avoiding that. Meaning: IMO it would make sense for the validation to also detect cycles introduced by extensions.

I you agree to that, do you also think that the current wording in the proposed spec ("Any directives provided must not contain the use of a Directive which references the previous directive directly") should be made more explicit to avoid an ambiguity here?

@yaacovCR

Copy link
Copy Markdown
Contributor Author

I’m not sure we actually need a change in wording, after reconsidering for a little bit. Although applied directors currently don’t exist, that could change in the future, and we need to preserve the option for directives or a subset of them to be preserved and therefore not cyclical. Will make changes!

@yaacovCR yaacovCR force-pushed the no-directive-definition-cycles-17 branch from 9494943 to e66c36d Compare June 12, 2026 14:13
@yaacovCR yaacovCR force-pushed the no-directive-definition-cycles-17 branch from e66c36d to 45bb6e5 Compare June 12, 2026 15:03
@yaacovCR yaacovCR merged commit f8ffad3 into graphql:17.x.x Jun 12, 2026
22 of 23 checks passed
@yaacovCR yaacovCR deleted the no-directive-definition-cycles-17 branch June 12, 2026 15:34
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 14, 2026
yaacovCR added a commit that referenced this pull request Jun 14, 2026
…#4815)

Haven't given up on this, but this new rule requires more polish and is
not going to make it into 17.x.x.
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 14, 2026
…l#4726

Adds a new `NoDirectiveDefinitionCyclesRule` SDL validation rule that
detects cycles in the graph of directives used within directive
definitions.

A cycle arises when following the references within directive
definitions leads back to the same directive. Two kinds of references
are tracked:

1. **Directive argument types** — if `@foo` takes an argument of type
`InputObject`, and `InputObject` has a field with directive `@foo`, that
forms a cycle: `@foo → InputObject → @foo`.
2. **Directives on directive definitions** — when the experimental
`experimentalDirectivesOnDirectiveDefinitions` parser option is enabled,
a directive applied directly to a directive definition or extension is
also a reference. If `@a` is annotated with `@b` and `@b` is annotated
with `@a`, that is a cycle: `@a → @b → @a`.

Cycles make it impossible to build a well-defined schema; the spec
already [forbids
them](https://spec.graphql.org/draft/#sec-Type-System.Directives.Type-Validation),
and experimental directive extensions [do the
same](https://github.com/graphql/graphql-spec/pull/1206/changes#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R2342).

perf(validation): prune directive cycle reference sources

refactor(validation): collect directive cycle references with visitors

fix(validation): include member references in directive cycle paths

fix(validation): avoid masking directive cycles with memoization

fix(validation): collect directive cycle references from schema objects

perf(validation): skip directive cycle rule when no directives can cycle
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 14, 2026
…l#4726

Adds a new `NoDirectiveDefinitionCyclesRule` SDL validation rule that
detects cycles in the graph of directives used within directive
definitions.

A cycle arises when following the references within directive
definitions leads back to the same directive. Two kinds of references
are tracked:

1. **Directive argument types** — if `@foo` takes an argument of type
`InputObject`, and `InputObject` has a field with directive `@foo`, that
forms a cycle: `@foo → InputObject → @foo`.
2. **Directives on directive definitions** — when the experimental
`experimentalDirectivesOnDirectiveDefinitions` parser option is enabled,
a directive applied directly to a directive definition or extension is
also a reference. If `@a` is annotated with `@b` and `@b` is annotated
with `@a`, that is a cycle: `@a → @b → @a`.

Cycles make it impossible to build a well-defined schema; the spec
already [forbids
them](https://spec.graphql.org/draft/#sec-Type-System.Directives.Type-Validation),
and experimental directive extensions [do the
same](https://github.com/graphql/graphql-spec/pull/1206/changes#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R2342).
yaacovCR added a commit that referenced this pull request Jun 15, 2026
## v17.0.0 (2026-06-15)

#### New Feature 🚀
* [#4819](#4819) feat: graduate directives on directives ([@yaacovCR](https://github.com/yaacovCR))

#### Bug Fix 🐞
* [#4799](#4799) fix: raise request error on invalid fragment variables ([@yaacovCR](https://github.com/yaacovCR))
* [#4800](#4800) fix: apply directives when SDL contains type definitions and extensions with directives ([@yaacovCR](https://github.com/yaacovCR))
* [#4564](#4564) OneOf Inhabitability ([@jbellenger](https://github.com/jbellenger))
* [#4814](#4814) fix(KnownDirectivesRule): locations for input field arguments in extensions ([@yaacovCR](https://github.com/yaacovCR))
* [#4726](#4726) feat(validation): reject directive definition cycles ([@yaacovCR](https://github.com/yaacovCR))
* [#4815](#4815) Revert "feat(validation): reject directive definition cycles (#4726)" ([@yaacovCR](https://github.com/yaacovCR))

#### Docs 📝
<details>
<summary> 11 PRs were merged </summary>

* [#4790](#4790) docs: minor diagnostics doc comment improvements ([@yaacovCR](https://github.com/yaacovCR))
* [#4791](#4791) fix: docs: polish diagnostics comments further ([@yaacovCR](https://github.com/yaacovCR))
* [#4793](#4793) docs: further improve general execution and tracing docs ([@yaacovCR](https://github.com/yaacovCR))
* [#4802](#4802) docs: correct extension field comments - v17 ([@yaacovCR](https://github.com/yaacovCR))
* [#4805](#4805) docs: publish fixed extensions comments ([@yaacovCR](https://github.com/yaacovCR))
* [#4807](#4807) docs: add prettier for jsdoc examples ([@yaacovCR](https://github.com/yaacovCR))
* [#4435](#4435) Subscriptions docs suggestions ([@Urigo](https://github.com/Urigo))
* [#4811](#4811) docs: fix a few indentations inside string literals ([@yaacovCR](https://github.com/yaacovCR))
* [#4813](#4813) internal: docs update ([@yaacovCR](https://github.com/yaacovCR))
* [#4820](#4820) docs: document `@experimental_disableErrorPropagation` ([@yaacovCR](https://github.com/yaacovCR))
* [#4817](#4817) docs: post 17.rc-0 update ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Polish 💅
<details>
<summary> 2 PRs were merged </summary>

* [#4809](#4809) internal: use prettier for non-generated website files ([@yaacovCR](https://github.com/yaacovCR))
* [#4812](#4812) polish: fix stream test cases ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Internal 🏠
<details>
<summary> 6 PRs were merged </summary>

* [#4795](#4795) chore: move website publishing from 16.x.x to 17.x.x ([@yaacovCR](https://github.com/yaacovCR))
* [#4796](#4796) internal: fix broken npm/deno deployments ([@yaacovCR](https://github.com/yaacovCR))
* [#4797](#4797) ci: update GitHub Actions versions ([@yaacovCR](https://github.com/yaacovCR))
* [#4806](#4806) internal: update frontmatter ([@yaacovCR](https://github.com/yaacovCR))
* [#4808](#4808) intenral: fix ci badge ([@yaacovCR](https://github.com/yaacovCR))
* [#4810](#4810) internal: add prettier:examples to lint-staged ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Committers: 3
* James Bellenger([@jbellenger](https://github.com/jbellenger))
* Uri Goldshtein([@Urigo](https://github.com/Urigo))
* Yaacov Rydzinski ([@yaacovCR](https://github.com/yaacovCR))
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 21, 2026
…l#4726

Adds a new `NoDirectiveDefinitionCyclesRule` SDL validation rule that
detects cycles in the graph of directives used within directive
definitions.

A cycle arises when following the references within directive
definitions leads back to the same directive. Two kinds of references
are tracked:

1. **Directive argument types** — if `@foo` takes an argument of type
`InputObject`, and `InputObject` has a field with directive `@foo`, that
forms a cycle: `@foo → InputObject → @foo`.
2. **Directives on directive definitions** — when the experimental
`experimentalDirectivesOnDirectiveDefinitions` parser option is enabled,
a directive applied directly to a directive definition or extension is
also a reference. If `@a` is annotated with `@b` and `@b` is annotated
with `@a`, that is a cycle: `@a → @b → @a`.

Cycles make it impossible to build a well-defined schema; the spec
already [forbids
them](https://spec.graphql.org/draft/#sec-Type-System.Directives.Type-Validation),
and experimental directive extensions [do the
same](https://github.com/graphql/graphql-spec/pull/1206/changes#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R2342).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: bug fix 🐞 requires increase of "patch" version number spec RFC Implementation of a proposed change to the GraphQL specification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants