Skip to content

Add support for @specifiedBy directive#1913

Merged
spawnia merged 18 commits into
masterfrom
copilot/rebase-on-master-address-review-comment
Jun 14, 2026
Merged

Add support for @specifiedBy directive#1913
spawnia merged 18 commits into
masterfrom
copilot/rebase-on-master-address-review-comment

Conversation

Copilot AI commented May 13, 2026

Copy link
Copy Markdown
Contributor

graphql-php parsed and validated @specifiedBy in SDL without ever doing anything with it. The URL was silently discarded, so custom scalar types had no way to carry their specification link through the library's own toolchain: it disappeared on introspection, on schema printing, on client-schema reconstruction, and on schema extension. Developers who relied on @specifiedBy to document their scalar types (e.g. scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")) received no error and no warning—the information simply vanished, making schema round-trips lossy and introspection-based tooling blind to the spec link.

The GraphQL specification and the reference implementation (graphql-js) have shipped @specifiedBy for several years. Closing this gap matters because:

  • Interoperability — schemas authored in other tools or languages include @specifiedBy; importing them via SDL or introspection into graphql-php must not silently drop data.
  • Self-documenting APIs — clients and schema-explorer tools query __type { specifiedByURL } to link scalar types to their machine-readable specification. Without this, graphql-php APIs are less discoverable.
  • Correctness of utilitiesBuildSchema, BuildClientSchema, SchemaExtender, and SchemaPrinter are all supposed to preserve the full semantics of a schema; losing specifiedByURL was a silent correctness bug in each of them.

This PR brings every layer of the library into alignment: the type definition, introspection, SDL parsing, schema extension, and printing all now treat specifiedByURL as a first-class, preserved property—matching graphql-js behaviour and the specification.

A latent bug in QueryComplexity was found and fixed in scope: directiveExcludesField() returned early on the first unrecognised directive, so a field annotated @foo @skip(if: true) was incorrectly included in complexity calculations. Variable coercion is now lazy and cached so it only runs when @include or @skip is actually present.

Copilot AI left a comment

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.

Pull request overview

Adds first-class support for the GraphQL @specifiedBy directive across schema construction, extension, and printing, so custom scalars can carry/emit a specifiedByURL and the directive appears among built-in directives (including introspection schema output).

Changes:

  • Introduces Directive::specifiedByDirective() / constants and registers it as a built-in directive; ensures BuildSchema includes it by default.
  • Threads specifiedByURL through scalar type config, SDL building (ASTDefinitionBuilder), schema extension (SchemaExtender), and SDL printing (SchemaPrinter).
  • Updates tests to reflect the new built-in directive and enabled specifiedBy-related behaviors.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Type/Definition/Directive.php Adds built-in @specifiedBy directive definition and constants.
src/Type/Definition/ScalarType.php Adds specifiedByURL to scalar config + instance property.
src/Type/Definition/CustomScalarType.php Updates PHPDoc config types to include specifiedByURL.
src/Utils/ASTDefinitionBuilder.php Extracts specifiedByURL from scalar AST directives and passes into scalar config.
src/Utils/BuildSchema.php Ensures @specifiedBy is included when not explicitly declared in SDL.
src/Utils/SchemaExtender.php Propagates specifiedByURL when extending scalars (incl. reading from extension AST).
src/Utils/SchemaPrinter.php Prints @specifiedBy(url: ...) on scalar definitions when present.
src/Language/Printer.php Adjusts string printing to avoid escaping unicode (and slashes) in JSON encoding.
src/Validator/Rules/QueryComplexity.php Attempts to ignore @specifiedBy during directive-based exclusion logic.
tests/Type/IntrospectionTest.php Updates expected introspection result to include specifiedBy directive.
tests/Utils/BreakingChangesFinderTest.php Includes specifiedByDirective() in removed-directive detection test.
tests/Utils/BuildSchemaTest.php Updates directive counts and enables specifiedBy-related schema build tests.
tests/Utils/SchemaExtenderTest.php Enables and extends scalar extension tests for @specifiedBy.
tests/Utils/SchemaPrinterTest.php Updates expected introspection schema output to include @specifiedBy directive definition.
tests/Validator/ValidatorTestCase.php Ensures test schema includes all built-in/internal directives via getInternalDirectives().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Validator/Rules/QueryComplexity.php Outdated
Comment thread src/Utils/ASTDefinitionBuilder.php Outdated
…ExcludesField

Agent-Logs-Url: https://github.com/webonyx/graphql-php/sessions/67e190bd-9ad8-4bc7-ac58-927893b6f552

Co-authored-by: spawnia <12158000+spawnia@users.noreply.github.com>

@github-actions github-actions 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.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: ccb5788 Previous: 516401c Ratio
DeferredBench::benchManyNestedDeferreds 21.856 ms 12.8 ms 1.71

This comment was automatically generated by workflow using github-action-benchmark.

…f), descriptions (behavior not behaviour)

Agent-Logs-Url: https://github.com/webonyx/graphql-php/sessions/5536e145-b412-4cca-9f9a-847ccaa65158

Co-authored-by: spawnia <12158000+spawnia@users.noreply.github.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Comment thread src/Type/Definition/ScalarType.php Outdated
Comment thread docs/type-definitions/directives.md Outdated
Comment thread src/Language/Printer.php Outdated
…rammar, Printer comment

Agent-Logs-Url: https://github.com/webonyx/graphql-php/sessions/ec97a2ec-237f-4065-aa53-2871b3ca7e17

Co-authored-by: spawnia <12158000+spawnia@users.noreply.github.com>
@spawnia spawnia marked this pull request as ready for review May 14, 2026 09:05
@spawnia spawnia requested a review from ruudk May 14, 2026 09:07
Copilot AI requested a review from spawnia May 14, 2026 09:10

@simPod simPod left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Found one behavior issue worth fixing before merge:

// SDL overrides the built-in directive shape
 directive @specifiedBy(url: String) on SCALAR
 scalar Foo @specifiedBy(url: "https://example.com/foo")

ASTDefinitionBuilder::getSpecifiedByURL() and SchemaExtender::extendScalarType() currently treat any directive named @specifiedBy with a string url argument as the built-in directive. That breaks the existing override behavior: once a schema defines its own @specifiedBy, BuildSchema correctly does not add Directive::specifiedByDirective(), but the scalar URL is still copied into ScalarType::$specifiedByURL and later exposed/printed as if it came from the built-in spec directive.

I think extraction should only happen when the active directive definition is the built-in @specifiedBy, not purely by directive name and argument shape. Otherwise custom directives with the same name leak into introspection/SDL semantics they did not opt into.

@spawnia

spawnia commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Re: review comment about custom @specifiedBy overrides —

This concern was addressed in 22c0ab1 — the extraction deliberately reads the URL directly from the AST (matching by directive name + argument name + StringValueNode) instead of using Values::getDirectiveValues(), which would coerce against the built-in definition and throw on incompatible overrides.

The current behavior when a schema overrides @specifiedBy with a compatible shape (same name, same url argument): the URL is still extracted. This is correct — if someone writes scalar Foo @specifiedBy(url: "https://example.com") using their own directive @specifiedBy(url: String) on SCALAR, the semantic intent is the same. Extracting it is harmless and useful.

When the override is incompatible (different argument name or non-string type), specifiedByURL stays null — no throw, no incorrect data.

This matches how graphql-js handles it: name-based matching with getDirectiveValues(GraphQLSpecifiedByDirective, node) which also does not guard against overrides.

@spawnia spawnia merged commit 913eea4 into master Jun 14, 2026
24 checks passed
@spawnia spawnia deleted the copilot/rebase-on-master-address-review-comment branch June 14, 2026 13:23
@spawnia

spawnia commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Released as v15.33.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants