-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
RFC: SemanticNonNull type (null only on error) #1065
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for graphql-spec-draft ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
Summary: This PR adds experimental support for `semanticNonNull` as described in apollographql/specs#42. Which is part of a broader effort to explore semantic nullability in GraphQL as explored in [RFC: SemanticNonNull type (null only on error) ](graphql/graphql-spec#1065). This directive-based approach should allow us to experiment with the concepts and identify issues as we work to understand the viability of semantic nullability in GraphQL. ## Experimental As this is still an experimental implementation, it's designed to be minimally invasive rather than ideal in terms of architecture or performance. As the feature/RFCs stabilize I would imagine we would bake this into the schema crate and data structures as a first class concept. This flag will not be broadly safe to enable in Relay since by default fields that are null due to error are still surfaced to the user. This is only safe to enable if: 1. Your network layer discards all payloads that have any field errors 2. You enable our [explicit error handling feature](#4416), which is still itself experimental. ## Missing Pieces - [ ] Documentation about how to use this feature (purposeful, since this is experimental) - [ ] Support for `semanticNonNullField` which allows a patching an existing type to define it's field's semantic nullability - [ ] Validation - [ ] Invalid use of `levels` will simply panic. - [ ] Uses of `semanticNonNullField` will simply be ignored - [ ] There is no schema validation ensuring interface types have type-compatible semantic nullability Pull Request resolved: #4601 Test Plan: ``` cargo test ``` I also spun up a version of [`grats-relay-example`](https://github.com/captbaritone/grats-relay-example) using Grat's [experimental support for `semanticNonNull`](https://grats.capt.dev/docs/guides/strict-semantic-nullability/) and was able to see it working end to end. https://github.com/facebook/relay/assets/162735/dc979a58-95f3-4e55-9d9b-577afdd798ca Reviewed By: alunyov Differential Revision: D53191255 Pulled By: captbaritone fbshipit-source-id: c09333f2b9475315d81792d33947fd908001c021
One outstanding question regarding this approach is "will we be able to find an additive syntax to denote 'null only on error' that is palatable?". Some folks from the Nullability Working Group met in an ad-hoc meeting to brainstorm the options available and itentify viable syntax candidates. (Meeting notes can be found here under the heading "Ad-hoc discussion on syntax") We arrived at a top three which seemed to pull away from the rest as clear leaders. The three leaders, including the properties that make them more or less desirable:
|
These are all really thoughtful. The only thing I have to add is that |
Also "Bang carrot" sounds kind of great :D |
You won't make any friends with syntax like that. 🙃 Adding a multi-character affix would be a big mistake, in my opinion. With @leebyron's proposal, this would simply be |
Along with
There's a number of reasons why using the unadorned type to represent a semantically non-null type can not be the goal; here's a few:
My personal opinion is that |
And that's a mistake IMO.
... in the right direction.
This is covered in Lee's proposal ("How to adopt this incrementally?"). You don't enable
Then they best be careful. 🙂
So nothing can ever be improved because people have written about GraphQL before? The language will become progressively worse while trying to achieve infinite BC. Is versioning really an impossibility here?
It means a client can continue to use optional chaining, sure, but how does the API suddenly start returning data that doesn't exist, if the field was previously nullable?
This is a migration concern. It's an issue once.
Part of the reason for these proposals is clients having to write code like
Again, I'm curious how this works in practice, and more importantly, how often it happens. What data do you return for a field that has been changed to non-nullable? A confusing default like
You're thinking of a scenario where the two coexist. You do the migration once, and align your mental model with many other languages, then you forget about the inconsistent syntax that GraphQL currently has (in relation to these languages and common conventions). Also, the behaviour might be toggled via a setting, and not necessarily via a schema directive hidden deep inside your project.
To the contrary, something like
This is adding more cognitive overhead – instead of just picking between nullable and non-nullable, we have to consider "semantically non-nullable" as well. How will that be less confusing?
Just Don't Do It™ It will make by-hand schema authoring a confusing mess. Multi-character affixes are awful. There exists a convention used commonly – adopt that, and align GraphQL with a common way of thinking. Accept the short-term pain for longer-term simplicity. If anything, involve the community – poll developers on:
|
@benjie I see your point regarding backwards compatibility and already produced GraphQL content.
type User {
name: String
communityName: String?
} The above is easily understood without knowing the historic context. type User {
id: ID!
name: String~!
communityName: String
} This is not. The introduction of the Similarly to how Besides the point that learning material would be outdated, I don't see a real reason why For me this would be an okay trade-off for an easier understood nullable syntax and shorter syntax for the common case. Contrary to you I think the new syntax would make adoption easier. For existing projects it's an optional, one time schema migration and for new projects you get an easier understood syntax out-of-the-box. |
TL;DR: Introduces a new type wrapper, Semantic-Non-Null, which represents that a value will not be null unless an error happens, and if an error does happen then this
null
does not bubble.The problem
GraphQL schema designers must use non-nullable types sparingly because if a non-nullable type were to raise an error then the entire selection set it is within will be destroyed, leading to clients receiving less usable data and making writing the results to a normalized cache a dangerous action. Because of this, nullable-by-default is a best practice in GraphQL, and non-null type wrappers should only be used for fields that the schema designer is confident will never raise an error - not just in the current schema, but in all future schemas.
Many GraphQL consumers choose to ignore the entire response from the server when any error happens, one reason for this is because the null bubbling behavior makes writing to normalized caches dangerous. For these users, when an error doesn't happen, the nullable fields they are dealing with can be frustrating because their type generation requires them to handle the null case even if it may never happen in practice, which can lead to a lot of unnecessary code that will never execute. There is currently no way for the type generators to know that a field will never be null unless there's an associated error.
The solution
We can categorise that there are effectively two types of
null
:null
: where a position isnull
and there's a related error (with matching or prefixed path) in theerrors
list - indicates that something went wrong.null
: where a position isnull
and there is no related error - this data truly is null (e.g. a user having not yet set their avatar may haveavatar: null
; this is not an error).This PR introduces a new wrapper type in addition to List and Non-Null, called Semantic-Non-Null. The Semantic-Non-Null type indicates that the field will never be a semantic
null
- it will not benull
in the normal course of business, but can be null only if accompanied by an error in theerrors
list (i.e. an "errornull
"). Thus a client that throws out all responses with errors will never see anull
in this position. Also, critically, anynull
raised by this field will not bubble and thus if an error is found with the exact path to thisnull
then it is safe to store the result (including the error) into a normalized cache.In SDL the Semantic-Non-Null wrapper is currently represented by a
!
prefix (as opposed to the!
suffix for a strict Non-Null).Thus we have the following:
1
String
null
, or semanticnull
2
!String
null
3
String!
Note that
1
and3
above are exactly the same as in the current GraphQL specification, this PR introduces2
which sits in the middle.Backwards compatibility
All existing schemas are automatically supported because the meaning of
String
andString!
is unchanged.To ensure that all existing clients are automatically supported, this PR introduces the
includeSemanticNonNull
argument on__Field.type
which defaults tofalse
. Clients that do not passincludeSemanticNonNull: true
will see all Semantic-Non-Null types stripped, which will have the effect of making them appear as if they were the unadorned types. This is safe, since it means these clients will need to handle both error nulls and semantic nulls (as they traditionally would have) even though we know that a semantic null will never happen in practice.All existing GraphQL documentation, tutorials, examples, and everything else we've built over the last 8 years remains valid since the meaning of
String
andString!
are unchanged.History
This PR is almost identical to #1048, but it changes the name of the new type wrapper from Null-Only-On-Error to Semantic-Non-Null, and changes the syntax from
String*
to!String
. It addresses the True Nullability Schema discussion raised by @captbaritone and incorporates/adapts some of the terminology from @leebyron's Strict Semantic Nullability proposal.