From cf4533bea44b12f92955a00e2dd3199fa81f966f Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 19 Mar 2026 12:56:46 -0400 Subject: [PATCH 1/8] Spec edits for incremental delivery, Validation --- spec/Section 5 -- Validation.md | 179 ++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index c48a6ba4a..aee87b90f 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -560,6 +560,7 @@ FieldsInSetCanMerge(set): {set} including visiting fragments and inline fragments. - Given each pair of distinct members {fieldA} and {fieldB} in {fieldsForName}: - {SameResponseShape(fieldA, fieldB)} must be true. + - {SameStreamDirective(fieldA, fieldB)} must be true. - If the parent types of {fieldA} and {fieldB} are equal or if either is not an Object Type: - {fieldA} and {fieldB} must have identical field names. @@ -595,6 +596,16 @@ SameResponseShape(fieldA, fieldB): - If {SameResponseShape(subfieldA, subfieldB)} is {false}, return {false}. - Return {true}. +SameStreamDirective(fieldA, fieldB): + +- If neither {fieldA} nor {fieldB} has a directive named `stream`. + - Return {true}. +- If both {fieldA} and {fieldB} have a directive named `stream`. + - Let {streamA} be the directive named `stream` on {fieldA}. + - Let {streamB} be the directive named `stream` on {fieldB}. + - If {streamA} and {streamB} have identical sets of arguments, return {true}. +- Return {false}. + Note: In prior versions of the spec the term "composite" was used to signal a type that is either an Object, Interface or Union type. @@ -1695,6 +1706,174 @@ query ($foo: Boolean = true, $bar: Boolean = false) { } ``` +### Defer And Stream Directives Are Used On Valid Root Field + +** Formal Specification ** + +- For every {directive} in a document. +- Let {directiveName} be the name of {directive}. +- Let {mutationType} be the root Mutation type in {schema}. +- Let {subscriptionType} be the root Subscription type in {schema}. +- If {directiveName} is "defer" or "stream": + - The parent type of {directive} must not be {mutationType} or + {subscriptionType}. + +**Explanatory Text** + +The defer and stream directives are not allowed to be used on root fields of the +mutation or subscription type. + +For example, the following document will not pass validation because `@defer` +has been used on a root mutation field: + +```raw graphql counter-example +mutation { + ... @defer { + mutationField + } +} +``` + +### Defer And Stream Directives Are Used On Valid Operations + +** Formal Specification ** + +- Let {subscriptionFragments} be the empty set. +- For each {operation} in a document: + - If {operation} is a subscription operation: + - Let {fragments} be every fragment referenced by that {operation} + transitively. + - For each {fragment} in {fragments}: + - Let {fragmentName} be the name of {fragment}. + - Add {fragmentName} to {subscriptionFragments}. +- For every {directive} in a document: + - If {directiveName} is not "defer" or "stream": + - Continue to the next {directive}. + - Let {ancestor} be the ancestor operation or fragment definition of + {directive}. + - If {ancestor} is a fragment definition: + - If the fragment name of {ancestor} is not present in + {subscriptionFragments}: + - Continue to the next {directive}. + - If {ancestor} is not a subscription operation: + - Continue to the next {directive}. + - Let {if} be the argument named "if" on {directive}. + - {if} must be defined. + - Let {argumentValue} be the value passed to {if}. + - {argumentValue} must be a variable, or the boolean value "false". + +**Explanatory Text** + +The defer and stream directives can not be used to defer or stream data in +subscription operations. If these directives appear in a subscription operation +they must be disabled using the "if" argument. This rule will not permit any +defer or stream directives on a subscription operation that cannot be disabled +using the "if" argument. + +For example, the following document will not pass validation because `@defer` +has been used in a subscription operation with no "if" argument defined: + +```raw graphql counter-example +subscription sub { + newMessage { + ... @defer { + body + } + } +} +``` + +### Defer And Stream Directive Labels Are Unique + +** Formal Specification ** + +- Let {labelValues} be an empty set. +- For every {directive} in the document: + - Let {directiveName} be the name of {directive}. + - If {directiveName} is "defer" or "stream": + - For every {argument} in {directive}: + - Let {argumentName} be the name of {argument}. + - Let {argumentValue} be the value passed to {argument}. + - If {argumentName} is "label": + - {argumentValue} must not be a variable. + - {argumentValue} must not be present in {labelValues}. + - Append {argumentValue} to {labelValues}. + +**Explanatory Text** + +The `@defer` and `@stream` directives each accept an argument "label". This +label may be used by GraphQL clients to uniquely identify response payloads. If +a label is passed, it must not be a variable and it must be unique within all +other `@defer` and `@stream` directives in the document. + +For example the following document is valid: + +```graphql example +{ + dog { + ...fragmentOne + ...fragmentTwo @defer(label: "dogDefer") + } + pets @stream(label: "petStream") { + name + } +} + +fragment fragmentOne on Dog { + name +} + +fragment fragmentTwo on Dog { + owner { + name + } +} +``` + +For example, the following document will not pass validation because the same +label is used in different `@defer` and `@stream` directives.: + +```raw graphql counter-example +{ + dog { + ...fragmentOne @defer(label: "MyLabel") + } + pets @stream(label: "MyLabel") { + name + } +} + +fragment fragmentOne on Dog { + name +} +``` + +### Stream Directives Are Used On List Fields + +**Formal Specification** + +- For every {directive} in a document. +- Let {directiveName} be the name of {directive}. +- If {directiveName} is "stream": + - Let {adjacent} be the AST node the directive affects. + - {adjacent} must be a List type. + +**Explanatory Text** + +GraphQL directive locations do not provide enough granularity to distinguish the +type of fields used in a GraphQL document. Since the stream directive is only +valid on list fields, an additional validation rule must be used to ensure it is +used correctly. + +For example, the following document will only pass validation if `field` is +defined as a List type in the associated schema. + +```graphql counter-example +query { + field @stream(initialCount: 0) +} +``` + ## Variables ### Variable Uniqueness From b4858e7ce0bc5d43ff98334d9ed111d1df6f35ec Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 14 Apr 2026 12:57:02 -0400 Subject: [PATCH 2/8] Update spec/Section 5 -- Validation.md Co-authored-by: Yaacov Rydzinski --- spec/Section 5 -- Validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index aee87b90f..1a26a40ab 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1797,7 +1797,7 @@ subscription sub { - If {argumentName} is "label": - {argumentValue} must not be a variable. - {argumentValue} must not be present in {labelValues}. - - Append {argumentValue} to {labelValues}. + - Add {argumentValue} to {labelValues}. **Explanatory Text** From e1efce59082b3daeb9b8ca686f8f1ae20b21d24c Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 30 Apr 2026 13:12:13 -0400 Subject: [PATCH 3/8] Update labels are unique rule to account for null value of label --- spec/Section 5 -- Validation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 1a26a40ab..28ae16ce0 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1795,6 +1795,8 @@ subscription sub { - Let {argumentName} be the name of {argument}. - Let {argumentValue} be the value passed to {argument}. - If {argumentName} is "label": + - If {argumentValue} is {null}: + - Continue to the next {argument}. - {argumentValue} must not be a variable. - {argumentValue} must not be present in {labelValues}. - Add {argumentValue} to {labelValues}. From da56a20c761f7679c344d4e93bc314ecfbc22d09 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 20 May 2026 09:28:58 -0700 Subject: [PATCH 4/8] PR feedback --- spec/Section 5 -- Validation.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 28ae16ce0..a4c48aaed 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1708,7 +1708,7 @@ query ($foo: Boolean = true, $bar: Boolean = false) { ### Defer And Stream Directives Are Used On Valid Root Field -** Formal Specification ** +**Formal Specification** - For every {directive} in a document. - Let {directiveName} be the name of {directive}. @@ -1720,8 +1720,8 @@ query ($foo: Boolean = true, $bar: Boolean = false) { **Explanatory Text** -The defer and stream directives are not allowed to be used on root fields of the -mutation or subscription type. +The `@defer` and `@stream` directives are not allowed to be used on root fields +of mutation or subscription operations. For example, the following document will not pass validation because `@defer` has been used on a root mutation field: @@ -1736,7 +1736,7 @@ mutation { ### Defer And Stream Directives Are Used On Valid Operations -** Formal Specification ** +**Formal Specification** - Let {subscriptionFragments} be the empty set. - For each {operation} in a document: @@ -1785,7 +1785,7 @@ subscription sub { ### Defer And Stream Directive Labels Are Unique -** Formal Specification ** +**Formal Specification** - Let {labelValues} be an empty set. - For every {directive} in the document: From 8debee82f17bcd3fbd515a5736c311e801a7e1ba Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 20 May 2026 09:29:27 -0700 Subject: [PATCH 5/8] Rewrite Valid Root Field rule --- spec/Section 5 -- Validation.md | 35 ++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index a4c48aaed..870428b03 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1710,13 +1710,34 @@ query ($foo: Boolean = true, $bar: Boolean = false) { **Formal Specification** -- For every {directive} in a document. -- Let {directiveName} be the name of {directive}. -- Let {mutationType} be the root Mutation type in {schema}. -- Let {subscriptionType} be the root Subscription type in {schema}. -- If {directiveName} is "defer" or "stream": - - The parent type of {directive} must not be {mutationType} or - {subscriptionType}. +- For each operation definition {operation} in the document: + - If the operation type is not subscription or mutation: + - Continue to the next operation definition. + - Let {selectionSet} be the top level selection set on {operation}. + - {CollectRootFields(selectionSet)}. + +CollectRootFields(selectionSet, visitedFragments): + +- If {visitedFragments} is not provided, initialize it to the empty set. +- For each {selection} in {selectionSet}: + - If {selection} is a {Field}: + - {selection} must not provide the `@stream` directive. + - If {selection} is a {FragmentSpread}: + - Let {fragmentSpreadName} be the name of {selection}. + - If {fragmentSpreadName} is in {visitedFragments}, continue with the next + {selection} in {selectionSet}. + - {selection} must not provide the `@defer` directive. + - Add {fragmentSpreadName} to {visitedFragments}. + - Let {fragment} be the Fragment in the current Document whose name is + {fragmentSpreadName}. + - If no such {fragment} exists, continue with the next {selection} in + {selectionSet}. + - Let {fragmentSelectionSet} be the top-level selection set of {selection}. + - {CollectRootFields(fragmentSelectionSet, visitedFragments)}. + - If {selection} is a {InlineFragment}: + - {selection} must not provide the `@defer` directive. + - Let {fragmentSelectionSet} be the top-level selection set of {selection}. + - {CollectRootFields(fragmentSelectionSet, visitedFragments)}. **Explanatory Text** From 14942afd5519842438bf09b9fc96f3b00b20a39e Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 21 May 2026 10:22:19 -0700 Subject: [PATCH 6/8] review fixes --- spec/Section 5 -- Validation.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 870428b03..bf6f99649 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1714,9 +1714,9 @@ query ($foo: Boolean = true, $bar: Boolean = false) { - If the operation type is not subscription or mutation: - Continue to the next operation definition. - Let {selectionSet} be the top level selection set on {operation}. - - {CollectRootFields(selectionSet)}. + - {ForbidDeferStream(selectionSet)}. -CollectRootFields(selectionSet, visitedFragments): +ForbidDeferStream(selectionSet, visitedFragments): - If {visitedFragments} is not provided, initialize it to the empty set. - For each {selection} in {selectionSet}: @@ -1732,12 +1732,12 @@ CollectRootFields(selectionSet, visitedFragments): {fragmentSpreadName}. - If no such {fragment} exists, continue with the next {selection} in {selectionSet}. - - Let {fragmentSelectionSet} be the top-level selection set of {selection}. - - {CollectRootFields(fragmentSelectionSet, visitedFragments)}. + - Let {fragmentSelectionSet} be the selection set of {selection}. + - {ForbidDeferStream(fragmentSelectionSet, visitedFragments)}. - If {selection} is a {InlineFragment}: - {selection} must not provide the `@defer` directive. - - Let {fragmentSelectionSet} be the top-level selection set of {selection}. - - {CollectRootFields(fragmentSelectionSet, visitedFragments)}. + - Let {fragmentSelectionSet} be the selection set of {selection}. + - {ForbidDeferStream(fragmentSelectionSet, visitedFragments)}. **Explanatory Text** From 59f4d645c657bbaa60faf3241e5d8ab6cfa8d606 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 21 May 2026 14:00:37 -0700 Subject: [PATCH 7/8] rewrite Valid Operations rule --- spec/Section 5 -- Validation.md | 61 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index bf6f99649..34e459fad 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1759,29 +1759,46 @@ mutation { **Formal Specification** -- Let {subscriptionFragments} be the empty set. +- Initialize {visitedFragments} to the empty set. - For each {operation} in a document: - - If {operation} is a subscription operation: - - Let {fragments} be every fragment referenced by that {operation} - transitively. - - For each {fragment} in {fragments}: - - Let {fragmentName} be the name of {fragment}. - - Add {fragmentName} to {subscriptionFragments}. -- For every {directive} in a document: - - If {directiveName} is not "defer" or "stream": - - Continue to the next {directive}. - - Let {ancestor} be the ancestor operation or fragment definition of - {directive}. - - If {ancestor} is a fragment definition: - - If the fragment name of {ancestor} is not present in - {subscriptionFragments}: - - Continue to the next {directive}. - - If {ancestor} is not a subscription operation: - - Continue to the next {directive}. - - Let {if} be the argument named "if" on {directive}. - - {if} must be defined. - - Let {argumentValue} be the value passed to {if}. - - {argumentValue} must be a variable, or the boolean value "false". + - If {operation} is not a subscription operation: + - Continue to the next operation definition. + - Let {operationSelection} be the top level selection set on {operation}. + - {ForbidUnconditionalDeferStream(operationSelection, visitedFragments)}. + +ForbidUnconditionalDeferStream(selectionSet, visitedFragments): + +- For each {selection} in {selectionSet}: + - If {selection} provides the `@skip` directive, and the "if" argument on that + directive is not the boolean value {false}: + - Continue to the next {selection} in {selectionSet}. + - If {selection} provides the `@include` directive, and the "if" argument on + that directive is not the boolean value {true}: + - Continue to the next {selection} in {selectionSet}. + - For each {directive} on {selection}: + - If {directive} is `@defer` or `@stream`: + - Let {if} be the argument named "if" on {directive}. + - {if} must be defined. + - Let {argumentValue} be the value passed to {if}. + - {argumentValue} must be a variable, or the boolean value "false". + - If {selection} is a {FragmentSpread}: + - Let {fragmentSpreadName} be the name of {selection}. + - If {fragmentSpreadName} is in {visitedFragments}, continue with the next + {selection} in {selectionSet}. + - Add {fragmentSpreadName} to {visitedFragments}. + - Let {fragment} be the Fragment in the current Document whose name is + {fragmentSpreadName}. + - If no such {fragment} exists, continue with the next {selection} in + {selectionSet}. + - Let {fragmentSelectionSet} be the selection set of {selection}. + - {ForbidUnconditionalDeferStream(fragmentSelectionSet, visitedFragments)}. + - If {selection} is an {InlineFragment}: + - Let {fragmentSelectionSet} be the selection set of {selection}. + - {ForbidUnconditionalDeferStream(fragmentSelectionSet, visitedFragments)}. + - If {selection} is a {Field}: + - Let {fieldSelectionSet} be the selection set of {selection}. + - If {fieldSelectionSet} exists: + - {ForbidUnconditionalDeferStream(fieldSelectionSet, visitedFragments)}. **Explanatory Text** From 68a1eb922c068d93998d34dc786c8c0897da2f9b Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 21 May 2026 17:41:43 -0700 Subject: [PATCH 8/8] review fixes --- spec/Section 5 -- Validation.md | 74 ++++++++++++++------------------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 34e459fad..046df66f4 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1755,7 +1755,7 @@ mutation { } ``` -### Defer And Stream Directives Are Used On Valid Operations +### Defer And Stream Directives Are Used In Valid Operations **Formal Specification** @@ -1769,18 +1769,17 @@ mutation { ForbidUnconditionalDeferStream(selectionSet, visitedFragments): - For each {selection} in {selectionSet}: - - If {selection} provides the `@skip` directive, and the "if" argument on that + - If {selection} provides the `@skip` directive, and the `if` argument on that directive is not the boolean value {false}: - Continue to the next {selection} in {selectionSet}. - - If {selection} provides the `@include` directive, and the "if" argument on + - If {selection} provides the `@include` directive, and the `if` argument on that directive is not the boolean value {true}: - Continue to the next {selection} in {selectionSet}. - - For each {directive} on {selection}: - - If {directive} is `@defer` or `@stream`: - - Let {if} be the argument named "if" on {directive}. - - {if} must be defined. - - Let {argumentValue} be the value passed to {if}. - - {argumentValue} must be a variable, or the boolean value "false". + - For each `@defer` or `@stream` {directive} on {selection}: + - Let {if} be the argument named `if` on {directive}. + - {if} must be defined. + - Let {argumentValue} be the value passed to {if}. + - {argumentValue} must not be the boolean value {false}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - If {fragmentSpreadName} is in {visitedFragments}, continue with the next @@ -1792,24 +1791,21 @@ ForbidUnconditionalDeferStream(selectionSet, visitedFragments): {selectionSet}. - Let {fragmentSelectionSet} be the selection set of {selection}. - {ForbidUnconditionalDeferStream(fragmentSelectionSet, visitedFragments)}. - - If {selection} is an {InlineFragment}: - - Let {fragmentSelectionSet} be the selection set of {selection}. - - {ForbidUnconditionalDeferStream(fragmentSelectionSet, visitedFragments)}. - - If {selection} is a {Field}: - - Let {fieldSelectionSet} be the selection set of {selection}. - - If {fieldSelectionSet} exists: - - {ForbidUnconditionalDeferStream(fieldSelectionSet, visitedFragments)}. + - Otherwise: + - Let {nestedSelectionSet} be the selection set of {selection}. + - If {nestedSelectionSet} exists: + - {ForbidUnconditionalDeferStream(nestedSelectionSet, visitedFragments)}. **Explanatory Text** -The defer and stream directives can not be used to defer or stream data in +The `@defer` and `@stream` directives can not be used to defer or stream data in subscription operations. If these directives appear in a subscription operation -they must be disabled using the "if" argument. This rule will not permit any +they must be disabled using an `if` argument. This rule will not permit any defer or stream directives on a subscription operation that cannot be disabled -using the "if" argument. +using an `if` argument. For example, the following document will not pass validation because `@defer` -has been used in a subscription operation with no "if" argument defined: +has been used in a subscription operation with no `if` argument defined: ```raw graphql counter-example subscription sub { @@ -1826,22 +1822,17 @@ subscription sub { **Formal Specification** - Let {labelValues} be an empty set. -- For every {directive} in the document: - - Let {directiveName} be the name of {directive}. - - If {directiveName} is "defer" or "stream": - - For every {argument} in {directive}: - - Let {argumentName} be the name of {argument}. - - Let {argumentValue} be the value passed to {argument}. - - If {argumentName} is "label": - - If {argumentValue} is {null}: - - Continue to the next {argument}. - - {argumentValue} must not be a variable. - - {argumentValue} must not be present in {labelValues}. - - Add {argumentValue} to {labelValues}. +- For every `@defer` and `@stream` {directive} in the document: + - Let {label} be the value of {directive}'s `label` argument. + - If {label} does not exist or is {null}: + - Continue to the next {directive}. + - {label} must not be a variable. + - {label} must not be present in {labelValues}. + - Add {label} to {labelValues}. **Explanatory Text** -The `@defer` and `@stream` directives each accept an argument "label". This +The `@defer` and `@stream` directives each accept an argument `label`. This label may be used by GraphQL clients to uniquely identify response payloads. If a label is passed, it must not be a variable and it must be unique within all other `@defer` and `@stream` directives in the document. @@ -1870,8 +1861,8 @@ fragment fragmentTwo on Dog { } ``` -For example, the following document will not pass validation because the same -label is used in different `@defer` and `@stream` directives.: +The following document will not pass validation because the same label is used +in multiple `@defer` and `@stream` directives: ```raw graphql counter-example { @@ -1892,11 +1883,10 @@ fragment fragmentOne on Dog { **Formal Specification** -- For every {directive} in a document. -- Let {directiveName} be the name of {directive}. -- If {directiveName} is "stream": - - Let {adjacent} be the AST node the directive affects. - - {adjacent} must be a List type. +- For every `@stream` {directive} in the document: + - Let {adjacent} be the AST node {directive} affects. + - Let {nullableFieldType} be the unwrapped nullable type of {adjacent}. + - {nullableFieldType} must be a List type. **Explanatory Text** @@ -1905,8 +1895,8 @@ type of fields used in a GraphQL document. Since the stream directive is only valid on list fields, an additional validation rule must be used to ensure it is used correctly. -For example, the following document will only pass validation if `field` is -defined as a List type in the associated schema. +For example, the following document will only pass validation if `field` +contains a List type in the associated schema. ```graphql counter-example query {