-
Notifications
You must be signed in to change notification settings - Fork 6
Add @semanticNonNull draft
#49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+171
−0
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| # @semanticNonNull Directive Specification | ||
|
|
||
| ## Introduction | ||
|
|
||
| :: This document specifies the `@semanticNonNull` directive, a schema directive | ||
| that indicates a field position is _semantically non null_: it is only null when | ||
| there is a matching error in the response `errors` array, and is otherwise | ||
| guaranteed to contain a value. | ||
|
|
||
| GraphQL's Non-Null type (`!`) serves two purposes at once: it asserts that a | ||
| position can never logically be null, and it dictates that, when an error occurs | ||
| in that position during execution, the resulting null is _propagated_ to the | ||
| nearest nullable parent. Because error propagation can erase large amounts of | ||
| otherwise-valid data, API authors frequently make fields nullable purely to | ||
| contain the blast radius of field errors — even when those fields are, in the | ||
| absence of an error, always present. | ||
|
|
||
| `@semanticNonNull` lets a schema separate these two concerns. A field may remain | ||
| nullable on the wire (so that an error in it does not propagate) while still | ||
| declaring that, absent a matching error, the position always contains a value. | ||
|
|
||
| **Example** | ||
|
|
||
| ```graphql example | ||
| type User { | ||
| # `name` is nullable on the wire, but is only ever null if `name` itself | ||
| # produced an error. Tooling may treat it as non-null otherwise. | ||
| name: String @semanticNonNull | ||
|
|
||
| # The list and each of its elements are semantically non null. | ||
| friends: [User] @semanticNonNull(levels: [0, 1]) | ||
| } | ||
| ``` | ||
|
|
||
| **Use Cases** | ||
|
|
||
| - Code generators may emit non-null types for semantically non-null positions | ||
| when the client handles field errors through an out-of-band mechanism (such as | ||
| a result type or an exception raised at the edge of the selection set), sparing | ||
| developers from null checks on positions that are only ever null on error. | ||
| - Schema authors may keep fields nullable for resilience (so that field errors | ||
| do not propagate and erase sibling data) without sacrificing the ability to | ||
| communicate that those fields are, in practice, always present. | ||
|
|
||
| **Relationship to `onError`** | ||
|
|
||
| The [`onError` proposal](https://github.com/graphql/graphql-spec/pull/1163) | ||
| allows a client to opt out of error propagation (`onError: "NULL"`), so that a field error produces | ||
| a localized {null} rather than propagating up to the nearest nullable parent. Once | ||
| error propagation is no longer a concern, the _Non-Null_ type (`!`) can be used | ||
| to mark every position where {null} is not a semantically valid value — the true | ||
| nullability of the schema. | ||
|
|
||
| Representing that nullability with `!` is not always possible for existing services, however. Adding | ||
| `!` to an existing field may increase the blast radius of an error for clients that have not opted | ||
| out of error propagation. | ||
|
|
||
| `@semanticNonNull` addresses this. Because it is additive metadata that does not | ||
| change the wire type, a nullability-aware client may read the true nullability of | ||
| a field and omit its non-null checks, while clients that are unaware of the | ||
| directive observe the field exactly as before. `onError` and `@semanticNonNull` | ||
| describe the same underlying truth — which positions are truly non-null — but | ||
| `@semanticNonNull` can be adopted incrementally without affecting existing | ||
| clients. | ||
|
|
||
| ## Definition | ||
|
|
||
| ```graphql | ||
| directive @semanticNonNull(levels: [Int!]! = [0]) on FIELD_DEFINITION | ||
| ``` | ||
|
|
||
| The `@semanticNonNull` directive indicates that a field's result is | ||
| _semantically non null_ at the indicated _levels_. | ||
|
|
||
| A position is _semantically non null_ if it is only {null} when there is a | ||
| matching error in the `errors` array of the response. In all other cases, the | ||
| position contains a value. | ||
|
|
||
| `@semanticNonNull` may only be applied to a _FieldDefinition_ whose type is | ||
| nullable at the indicated _levels_. Applying it to a position that is already a | ||
| _Non-Null_ type at that level is meaningless (the position is already | ||
| guaranteed non-null by the type system) and must be considered an error. | ||
|
|
||
| ## The levels argument | ||
|
|
||
| The _levels_ argument selects which _levels_ of a (possibly nested) _List_ type | ||
| are semantically non null. Levels are zero-indexed, where level {0} is the | ||
| outermost position (the field's own value). | ||
|
|
||
| - For a non-list type, only level {0} is meaningful, and it refers to the | ||
| field's value itself. | ||
| - For a _List_ type, level {0} refers to the list itself, level {1} refers to | ||
| each element of the list, level {2} to each element of each nested list, and | ||
| so on. | ||
|
|
||
| The default value of `[0]` makes only the outermost position semantically non | ||
| null, matching the common case of a non-list field. | ||
|
|
||
| A _level_ that is negative, or that exceeds the list dimensionality of the | ||
| annotated field, must be considered an error. | ||
|
|
||
| **Examples** | ||
|
|
||
| Given the field type {"[[String]]"}: | ||
|
|
||
| | Application | Semantically non-null positions | | ||
| | ------------------------------------- | -------------------------------------------------- | | ||
| | `@semanticNonNull` | the outer list | | ||
| | `@semanticNonNull(levels: [1])` | each inner list | | ||
| | `@semanticNonNull(levels: [2])` | each {String} element | | ||
| | `@semanticNonNull(levels: [0, 1, 2])` | the outer list, each inner list, and each {String} | | ||
|
|
||
| ```graphql example | ||
| type Query { | ||
| # The list itself is semantically non null; individual elements may be null. | ||
| tags: [String] @semanticNonNull | ||
|
|
||
| # Both the list and each element are semantically non null. | ||
| ids: [ID] @semanticNonNull(levels: [0, 1]) | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # GAP-49: @semanticNonNull Directive | ||
|
|
||
| ## Overview | ||
|
|
||
| This proposal defines the `@semanticNonNull` directive, a schema directive that | ||
| marks a field position as _semantically non null_: the position is guaranteed to | ||
| contain a value unless there is a matching error in the response's `errors` | ||
| array. | ||
|
|
||
| This separates two concepts that GraphQL's `!` (Non-Null) type conflates: | ||
|
|
||
| - **"this position can never logically be null"** (semantic nullability), and | ||
| - **"this position becomes null when the field errors"** (error nullability). | ||
|
|
||
| By expressing semantic nullability separately, code generation tools can emit | ||
| non-null types for clients that can opt-out of error propagation with `onError: NULL`, removing the unnecessary null checks that | ||
| clients would otherwise have to write. | ||
|
|
||
| ## Relationship to prior art | ||
|
|
||
| This directive is part of the broader | ||
| [GraphQL Nullability specification](https://specs.apollo.dev/nullability/) being | ||
| developed in the | ||
| [GraphQL Nullability Working Group](https://github.com/graphql/nullability-wg). | ||
| The full nullability specification also defines client-side directives | ||
| (`@catch`, `@catchByDefault`) describing how clients handle field errors. This | ||
| GAP scopes itself to the schema-side `@semanticNonNull` (and the companion | ||
| `@semanticNonNullField`) directive, which is independently useful and can be | ||
| adopted on its own. | ||
|
|
||
| Related discussions and prior art: | ||
|
|
||
| - [GraphQL Nullability WG](https://github.com/graphql/nullability-wg) | ||
| - [graphql-spec #1452 — "Client Controlled Nullability"](https://github.com/graphql/graphql-spec/pull/1452) | ||
| and the broader nullability discussions in the GraphQL specification. | ||
| - [`specs.apollo.dev/nullability/v0.4`](https://specs.apollo.dev/nullability/v0.4/#@semanticNonNull). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| id: 49 | ||
| title: "@semanticNonNull Directive" | ||
| summary: > | ||
| A schema directive that marks a field position as "semantically non null" — | ||
| guaranteed to be non-null unless there is a matching error in the response | ||
| `errors` array — allowing tooling to generate non-null types for newer clients | ||
| without breaking older clients that cannot enable `onError: "NULL"`. | ||
| status: proposal | ||
| authors: | ||
| - name: "Martin Bonnin" | ||
| email: "martin@apollographql.com" | ||
| githubUsername: "@martinbonnin" | ||
| sponsor: "@martinbonnin" | ||
| discussion: "https://github.com/graphql/gaps/pull/49" | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping @captbaritone, @itamark, feels like you should be here. Is it OK if I add you?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@martinbonnin sponsors have to be part of @graphql/gaps-editors (in order to have write access on this repo). You are also able to be your own sponsor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good everything works for me. I'm down to merge this and we can add Jordan/Itamar later on if/when they want.