Replies: 5 comments 15 replies
-
I think what you're trying to address here makes sense. But I'd appreciate if you would humour my opinion on syntax. I apologise in advance if I have any facts or details wrong. As pointed out, in traditional GraphQL, there is The purpose of this discussion is to produce a middle-ground, where the syntax "strict non-null" is changed to double exclamation point ( I personally think that it'd be a mistake to change the two currently existing poles. Syntax to express new functionality should also be new. The countless schemas that already define single exclamation points should continue to do, and behave as they always have. Think "opt-in". I appreciate you have addressed this I saw a user on Twitter (we all know it's not X) claim that they were sad that Consequently, I have a suggestion: for me, TL;DR: I think strict non-null should remain as |
Beta Was this translation helpful? Give feedback.
-
One of the things I like the most about GraphQL is that it's a lingua franca allowing client and server developers to speak the same language. If the client now sees "Client SDL", that is not the same as the server, this significantly increases the risk of miscommunication and the overall cognitive load associated with GraphQL. Q. What do we want to optimize for?
Apollo Kotlin really wants to be 1. and read I get the compatibility concern. Everything is always a trade off. In that case, the trade off is I would personally favor the future (i.e. less |
Beta Was this translation helpful? Give feedback.
-
Going back to this slide:
I would favor option 2 because I don't believe the rationale for nullable being the default still holds in a world where semantic non nullability and no-null-bubbling exist. I'm afraid that with option 1, most schemas will continue to have all or almost all fields nullable (simply due to the syntax), when really, most fields should be semantically non null. |
Beta Was this translation helpful? Give feedback.
-
This proposal has been updated with the outcomes of last night's WG meeting (replay; agenda; notes), in particular |
Beta Was this translation helpful? Give feedback.
-
I still prefer syntax option 2. A change like this is not something that will be possible again any time soon, so it makes sense to make use of this opportunity to simplify the syntax.
|
Beta Was this translation helpful? Give feedback.
-
It is recommended that you watch the presentation of this proposal on YouTube (or at least look through the slides) before you read through the specifics below.
Note that this is an updated and converted version of this Google Doc which is now read-only but contains interesting comments and questions from earlier versions.
Note: there is a "key takeaways" section at the bottom.
This is an alternative solution to the "true nullability schema" problem, intended to reduce the need for client-controlled nullability (by indicating the semantic nullability of a field) and to work better with clients such as Relay who wish to opt-out of server-side null-bubbling (perhaps implementing it client-side when the relevant fields are read, or simply throwing out the entire response when an error occurs).
Lee and Benjie had the two leading solutions to this problem (Strict Semantic Nullability and SemanticNonNull type respectively) but neither were happy with the trade-offs of either solution. They arranged a call and discussed the problem from the ground-up, revisiting all the problems and solutions that led to this point, and ultimately resulting in this alternative solution that addresses the weaknesses of both proposals.
The key part of this proposal is that it says there are actually two "presentations" of the schema:
This solution mixes together all of the existing solutions and revisits some basic assumptions.
Error-handling clients
This proposal is based around splitting clients into two classes:
data
key in the response, do not factorerrors
into their understanding of nullability.null
in a non-nullable position insidedata
, even if an error occurs.data
anderrors
together, and understand how to correlate them.null
in a non-nullable position withindata
so long as it is accompanied by an entry with matching path inerrors
(cross-referencing error paths maintains type safety).fetch()
orcurl
based client, or even past Relay).For non-error-handling clients, the existing behavior of GraphQL is desired — they want to be able to trust the non-nullability of a field in the response directly (e.g. for type safety), and for that null bubbling is essential.
"Smart" clients typically manipulate the outgoing request (e.g. adding
__typename
or stripping client schema extension related fields/directives/etc), take the result from the server and write it to some kind of normalized store, and reassemble the result from their store (mixing in client-side schema extensions as appropriate). Currently it's unsafe for smart clients to write results to their store where null bubbling has occurred server-side. Since these clients are reassembling the result anyway, they are well positioned to re-implement any error/null bubbling behavior locally, or even to apply alternative behaviors such as throwing when the errored field is accessed by rendering code — they would like the choice of being able to opt-out of null bubbling altogether, and take control of how errors are handled themselves. "Smart" clients include Relay, Apollo Client, urql."Success-only" clients will discard the response on any error, whether it bubbles or not, and thus they'd rather see fields that are "null only on error" marked as non-nullable.
Semantic non-null (aka "null only on error")
Types in GraphQL schemas up until this point have been nullable by default, with a "non-null" type wrapper available. From now on we'll refer to this traditional "non-null" type wrapper as a "strictly non-null" type wrapper — this type wrapper says that a null cannot occur in this position, even in the case of an error.
For "success-only" clients and "smart" clients, by opting out of server-side null bubbling and handling errors locally (either by discarding the entire response as is the case for "success-only" clients, or re-implementing error bubbling locally as for "smart" clients), a new desire arises: the ability to see which fields/positions will only be null if an error occurs. Such positions are called "semantically non-null positions" — they will never be
null
unless an error has occurred.**If null-bubbling is disabled then all strictly non-null positions become semantically non-null positions. **When there is no null bubbling, all positions become nullable, and thus with no null bubbling "strictly non-null" would have no observable difference from "semantic non-null".
Importantly (since this is what the previous proposals missed), API consumers should not be exposed to this complexity. Frontend developers just want to know "can this field be null or not" so they know whether they need to handle nulls. (They may also want to use advanced techniques such as the @catch directive to choose how errors in that field are handled.)
Two modes
Each GraphQL request must operate in one of two modes:
In traditional mode, nulls cannot occur at non-nullable positions, and thus null-bubbling is implemented. Types are either nullable or strictly non-nullable (never null).
In no-null-bubbling mode, there are no non-nullable positions (since any error that occurs cannot bubble, and thus every field/position becomes nullable). Types are either nullable or semantically non-nullable (null only on error).
Syntax
We often describe the schema to clients and client-side tooling using the SDL syntax. How can we represent this new behavior in SDL?
In traditional mode, types are either nullable or strictly non-nullable. We represent nullable as
Type
and strictly non-nullable asType!
.In no-null-bubbling mode, types are either nullable or semantically non-nullable. We represent nullable as
Type
and semantically non-nullable asType!
. To indicate the difference in interpretation of the!
, the SDL document will be marked with the@semanticNullability
directive (see below).Only one type of nullability happens in each type of request, so there is no conflict. The fact there are two types of nullability in the schema itself is a concern reserved for schema designers; consumers only ever have to deal with the type of nullability that they have chosen (assuming the server supports it).
Introspection
Introspection will honor the two request modes; if a request is performed in traditional mode then semantically non-nullable positions will be treated as nullable (no wrapper) and strictly non-nullable positions will be returned with a
NON_NULL
wrapper. In no-null-bubbling mode both strictly and semantically non-nullable positions will be returned with aSEMANTIC_NON_NULL
wrapper, since the behavior of both is equivalent in this mode.From a client perspective:
If a client really needs to know the difference (for example, in order to reconstruct a schema in full, or to re-implement traditional mode locally whilst safely writing to a normalized store) then it may pass an argument to the
__Field.type
introspection field in order to see the underlyingNON_NULL
andSEMANTIC_NON_NULL
wrappers.Writing a schema
When composing a schema, we want to make it convenient for the author to differentiate between semantically non-nullable positions and strictly non-nullable positions. One way to do this would be to use
!
to represent semantically non-nullable and!!
to represent strictly non-nullable (syntax open to bike-shedding, but this feels right to me). However, we want to ensure that the meaning of!
is not changed for existing documents. In particular, a schema might be constructed from a large number of existing parts, some of which the schema author may not have control over (e.g. those imported from external sources or from legacy modules/plugins, or even client-side schema extensions). Thus the fact of!
being interpreted as semantically non-nullable rather than strictly non-nullable cannot be a property of the schema itself (e.g. via a@strictNullability
directive) but must be a property of the document.I propose that we introduce directives at the top of a GraphQL document:
@semanticNullability
These document-level directives act as pragmatic directives and must appear before any other keywords. They can alter the way in which a document is interpreted.
@semanticNullability
would cause!
to be interpreted as semantically non-null and!!
to be interpreted as strictly non-null. Without this directive,!
will mean strictly non-null as it traditionally has, and the!!
syntax would be invalid and raise a validation error. When a document is composed with@semanticNullability
, input positions allow both!
and!!
but see them as equivalent: "non-null" - there's no difference between semantic and strict non-null on inputs. When SDL is generated (e.g. when printing a schema),!
should be used for input positions for consistency.Summary table
A schema contains two types of non-nullability (strict non-null, and semantic non-null). If you are writing a GraphQL schema by writing SDL (rather than code-first or similar) then you must use
@semanticNullability
in order to use the semantic non-null type wrapper. Without this, you will only have access to the traditional "strict" non-null type wrapper.@semanticNullability
Type
Type!
Type!!
Clients perform all requests using one of two modes: traditional mode, or no-null-bubbling mode. Clients understand that the mode that they chose impacts the behavior of the request, thus when they perform introspection and generate an SDL from it, what they produce is based on the mode with which they view the schema. Note that clients don't need to see the
!!
syntax - traditional clients only see strict nullability, and clients with null bubbling disabled will only observe semantic nullability. Here are how the two types of non-nullability are presented to a client based on which mode they are in:Client SDL Syntax
Possible runtime valuesClient SDL Syntax
Possible runtime valuesInt
Int
int | null
Int
int | semantic_null | error_null
Int
Int
int | null
Int!
int | error_null
Int
Int!
int (nulls bubble)
Int!
int | error_null
In traditional mode a client only cares if a position can have a null or not, irrespective of whether it comes with an error. Thus nullable and semantic non-null appear as identical in the client's eyes. In this mode, we know that the semantic non-null position will never have a
null
unless an error occurs, but the client does not know nor need to know this. (If it did, it should use no-null-bubbling mode.)In no-null-bubbling mode, a client recognizes that any field can produce an error_null (a null with associated error) and thus both strict-non-null positions and semantic non-null positions present as exactly the same. In this mode there is no observable difference between a strict-non-null and a semantic-non-null position, so there is no need for the client to differentiate between them.
Request/Response
When a smart client makes a request to a server it may include an additional parameter (e.g.
{query: "{__typename}", nullBubbling: false}
) and if the server supports no-null-bubbling mode it can indicate this in the response{data: {__typename: "Query"}, nullBubbling: false}
(if it does not do this, then it must not use semantic nullability). If the server does not supportnullBubbling: false
it will simply ignore that part of the request, and thus the response must be treated as having null bubbling enabled as is traditional.Terminology
All terminology in this document is open to bike-shedding! For example
nullBubbling: false
seems undesirable, a shorter/simpler option may be better. (errorHandling: false
?onError: "null"
?nullability: "SEMANTIC"
?)Key take-aways
!
to indicate their version of non-nullable (either strictly or semantically non-nullable, depending on mode).!
), and an additional set of fields may also be marked non-null (!
) if they are "null only on error" (semantically non-null).NON_NULL
will only appear in null-bubbling mode andSEMANTIC_NON_NULL
will only appear in no-null-bubbling mode.@semanticNullability
directive may be added on a document-by-document basis, and allows that document to mark a position as semantically non-null (with!
) or strictly non-null (with!!
). Documents without this directive retain their original meaning (i.e.!
means strictly non-null, and!!
is a validation error).@semanticNullability
must be used to indicate the changed meaning of the!
.Beta Was this translation helpful? Give feedback.
All reactions