-
Notifications
You must be signed in to change notification settings - Fork 2
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
Query level nullability review and drafting thread #1
Conversation
More of an explanation of the semantics of the swift force unwrap operator
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.
Thank you for writing this up! I much prefer !
over @nonNull
.
The current proposal does not describe how the server should behave when a !
marked field is null. Shouldn't that be part of the proposal?
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.
great work so far compiling all of the information from various docs 👏 some nits, some more high level stuff -- I can definitely help rework this stuff!
just a thought, shouldn't the proposal be named |
Co-authored-by: Liz Jakubowski <[email protected]>
Co-authored-by: Liz Jakubowski <[email protected]>
Co-authored-by: Liz Jakubowski <[email protected]>
Co-authored-by: Liz Jakubowski <[email protected]>
Co-authored-by: Liz Jakubowski <[email protected]>
Co-authored-by: Liz Jakubowski <[email protected]>
Co-authored-by: Liz Jakubowski <[email protected]>
For example, everything in this tree would be non-nullable | ||
```graphql | ||
query! { | ||
business(id: 4) { | ||
name | ||
} | ||
} | ||
``` |
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.
@wxue Do you remember why we didn't want this?
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.
I think we should remove this as an alternative, I don't think we understand it well enough to vouch for it
rfcs/QueryLevelNullability.md
Outdated
## 🗳️ Alternatives considered | ||
|
||
### Make Schema Fields Non-Nullable Instead | ||
Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) |
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.
TODO: Flesh this out with things about versioning, breaking changes, and the impact it has on partial results
Reworked some of the language to make it more generally applicable
Co-authored-by: Alex Reilly <[email protected]>
Hey! We shipped something like this in Relay last half. We haven’t yet documented it in open source, but it’s in wide use inside Facebook. I’ll share a description of what we built, and our observations below. APIOur approach was to include a custom directive,
In Relay these the custom Observations
|
Thank you so much for your attention and participation. I am excited to hear that Facebook has run into the same issue and implemented a solution that works at scale. I have a couple of questions to understand your comment better:
type Cat {
name: String
}
type Fish {
weight: String
}
union AnimalResult = Cat | Fish
type Query {
animal: AnimalResult
} query getAnimal {
animal {
... on Cat {
name @required(action: THROW)
}
... on Fish {
weight
}
}
} type GetAnimalQuery = { __typename: 'Cat', name: string } | { __typename: 'Fish', weight: string | null; } | null |
No. There might be a potential small win in terms of transfer size or server resources by not computing some other fields, but this hasn't stood out as a large enough potential win yet.
The type generation gets more weird when union or interface types are involved: |
No, our implementation is currently purely on the client, and Relay Compiler removes the One consideration here is that servers generally flatten away fragments during execution, but preserving the semantics of our
Fields can have ambiguous required-ness regardless of the presence of arguments. Consider:
Note that in this case the problem is (like in the previous point) the fact that (inline) fragments are spread into parents. If the results of However, the following is clearly ambiguous and should be considered invalid:
The challenge is inline fragments on abstract types - because the results of inline fragments are flattened into the outer result object, developers have to check for the presence of keys to see whether the object matched the abstract type. Consider a variation of your example:
Here is is possible to define types that represent the possible outcomes, but the possible combinations grow quickly as users add more required fields within more different type conditions (inline fragments). |
rfcs/QueryLevelNullability.md
Outdated
In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an error | ||
to the client rather than any data. |
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.
Can you expand here? My reading of this is that if the value of a field with !
is null at runtime, then the GraphQL executor must treat that identically to the case that the field resolver raised an exception, including returning an entry in the top-level errors
list and nulling out the parent field (which may need to then bubble up an error if that field was non-nullable in the schema, or also had !
applied).
This would mean that in a query such as the following, other_media_field
being null at runtime would cause the entire query to return effectively no data (just {data: {media: null}, errors: [...]}
). This would prevent any part of the UI from rendering because a single field was missing for one part:
query MediaQuery {
media(id: "...") {
name
thumbnail
...MediaFragment
}
}
fragment MediaFragment on Media {
other_media_field!
}
How are you thinking about handling cases such as this?
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.
I read this as any field in the response that would be null
and has a !
on the field in the operation would cause the whole operation to return an error and no data. Even if that field is in some named fragment.
I think this would limit the usefulness extremely, basically to very simple queries. To take Yelp as an example, I don't think it'd be a good idea to error a search query if a single search result item failed to generate a star rating (assuming that's required to render a search result item).
It would be good to clarify what the behavior is if the !
is in a named fragment.
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.
At Netflix, we have implemented this on the server (using a directive syntax) and defined it to have identical semantics as the existing SDL Non-Null behavior. This has worked well.
I've proposed a clarification to specify this.
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.
@fotoetienne Thanks for adding the clarification, that confirms my interpretation and brings us back to my original question:
How are you thinking about the fact that a single fragment with a !
field can cause data for other fragments (or in the extreme, an entire request) to be nulled out? As @captbaritone mentioned above, one of our goals / requirements when designing @required
was that a "missing" required value for one part of a UI cannot cause data for other parts of the UI to be nulled out. The design here violates that constraint (it breaks fragment modularity).
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.
@josephsavona That's a great question. Our current server logic will make a field act as Non-Null is there any Non-Null designation for that field in the query. This query example:
fragment Preview on Media {
thumbnail(size: 32)
... on Photo {
thumbnail(size: 32) @nonNull
}
}
would essentially be interpreted by the server as:
fragment Preview on Media {
thumbnail(size: 32) @nonNull
... on Photo {
thumbnail(size: 32) @nonNull
}
}
It is at least non-ambiguous, but I can see how it is a challenge for fragment modularity.
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.
@fotoetienne Thanks for clarifying. That seems like a severe limitation that would make applications significantly less robust than they can and arguably should be. How do you manage this in practice? Do you encourage fields with !
to be given unique aliases so that you prevent removing data for other parts of a UI?
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.
Short answer - it hasn't come up for us yet, but it's an important edge-case to consider.
Long answer - Fragment modularity is particularly crucial for clients that rely heavily on components tied to individually defined Fragments that are compiled into large queries. We don't currently have many teams using that approach.
I agree that this is an important edge-case that we should think through some more. We want a design that will lead UI developers to fall into "the pit of success" rather than allowing surprising behavior like you've described.
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.
An initial thought, probably flawed: reversing the server-defined logic might help. Only honor nonNull if all references to that field specify it. This would, however, require client codegen to be aware of an entire query and only make types nonNull if all fragments in the query specify nonNull. Thoughts?
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.
Making conflicting definitions between fragments be a validation error (like you did with @requires) is the safest approach. I like that. It's probably an acceptable limitation that you just can't have differing nullability for the same field.
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.
@joesavona even unique aliases would still null out the object by bubbling up to the parent. It's really the same as any specific field being non-nullable in the server schema and ending up null which bubbles up.
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.
Great work! Made a couple revisions/suggestions
|
||
This syntax consciously does not cover the following use cases: | ||
|
||
- **Default Values** |
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.
Defaults can be quite useful from a resilience standpoint. It is relevant to this proposal in that the !
syntax would't be as amenable to specifying a default, whereas a directive (e.g. @nonNull(default: false)
) would open us up to that.
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.
Though replacing !
with ! = “default”
like in variable defaults could work. This would either have to be evaluated client side (like in Relay) or the same default would have to apply to all references at the same location (whether from fragments or directly).
|
||
## 📜 Problem Statement | ||
|
||
The two modern languages used on mobile clients, Swift and Kotlin, are both non-null by default. By contrast, in a |
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.
I would consider reducing the focus on specific client languages.
The crux of the problem is that the SDL nonNull (!) eliminates the possibility of partial failure on a given type. This forces the SDL author to decide for which fields partial failure is acceptable. A GraphQL schema author may not be in the best position to decide whether partial failure is acceptable for a given canvas. Additionally, the answer to whether a missing value is acceptable may vary between UI canvases. Specifying nonNull (aka. required) in the query gives the UI developer the power to decide where partial failure is acceptable, and the flexibility for different canvases to have different requirements in this regard.
The pains are more noticeable in languages with first-class null safety, but are relevant to any client.
Co-authored-by: Stephen Spalding <[email protected]>
This edit adds a distinct Behavior section that specifies how a client-side Non-Null operator functions This is separated from the Syntax proposal to avoid confusion.
Co-authored-by: Alex Reilly <[email protected]>
Co-authored-by: Alex Reilly <[email protected]>
Co-authored-by: Alex Reilly <[email protected]>
Formally specify Non-Null behavior
|
||
While this solution simplifies some client-side logic, it does not meaningfully improve the developer experience for clients. | ||
|
||
* The cache implementations of GraphQL client libraries also need to understand the custom directive to behave correctly. Currently, when a client library caches a null field based on an operation without a directive, it will return the null field for another operation with this directive. |
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.
This will be true even with custom syntax. For composability of fragments, it would be worrisome if this new syntax made it impossible to have these two fragments in the same application:
fragment UserOkWithoutName on User {
name
}
fragment UserMustHaveName on User {
name!
}
With consistency, I could end up re-rendering a component built with UserMustHaveName
based on a response fetched using only UserOkWithoutName
.
And in fact it gets worse:
fragment BestFriendWithoutName on User {
best_friend {
id
}
}
fragment BestFriendWithName on User {
best_friend {
id
name!
}
}
If my best friend, when I fetched BestFriendWithName
, is User:1
, and then suddenly, after fetching BestFriendWithoutName
is User:2
, I no longer am capable of rendering the component relying on BestFriendWithName
(because name
is unset on User:2
). If the client library doesn't know what to do in that case, you can end up with consistency updates causing crashes.
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.
As you said, the client library will need to understand this new syntax/directive. I am not familiar with GraphQL client implementations, so I am not sure how difficult my imagined behavior will be to implement. Doesn't this behavior solve the issue you outlined?
If a GraphQL client finds that a component relies on
BestFriendWithName
, but the cached data doesn't havename
, it should not return any of the cached data. To the client, it is like a state before fetching started. Depending onfetch-policy
(to use Apollo's terminology), the client can start a new request to fetch the missing field data.
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.
Doesn't this behavior solve the issue you outlined?
That's how Apollo solves it, but that strategy isn't true universally. For instance that's not how Relay solves it. The client I work on can't solve it this way, as our consistency subscriptions are at the response level, for queries that include hundreds of fragments (whether that's a good choice or not is irrelevant: it's how things work for the largest GraphQL client at Facebook): if missing name
meant the entire consistency update should not be served, whenever a product chose to mark their field with !
, they could cause entire unrelated surfaces to lose consistency.
Another valid strategy would also be to return the data, but with best_friend
being null: all the other data in the fragment/consistency subscription would still be delivered (this is Relay's strategy for @required(action: NONE)
). Or requiring the product to null check every piece of data, always, regardless of its nullability annotation.
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.
Do you mean that Relay requires field selections on the same id and type to behave the same globally? If so, I can see how my suggested behavior won't work. If not:
Another valid strategy would also be to return the data, but with best_friend being null: all the other data in the fragment/consistency subscription would still be delivered (this is Relay's strategy for @required(action: NONE))
This is what I imagined. The client would nullify up until the most immediate nullable parent.
There are cases where a field is `nullable`, but a feature that fetches the field will not function if it is `null`. | ||
For example if you are trying to render an information page for a movie, you won't be able to do that if the name field | ||
of the movie is missing. In that case it would be preferable to fail as early as possible. |
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.
Here's one of the cases where this would be needed.
GatsbyJS projects use GraphQL to handle build-time data dependencies, and provide a file system route API that can automatically build pages from specific types.
Creating a file called src/pages/{BlogPost.slug}.jsx
creates pages from all BlogPost nodes and injects page context. { id: string, slug: string }
user can use the page context as an argument to fetch additional data.
# $id is automatically injected by Gatsby
query BlogPostQuery($id: String!) {
# The type of blogPost is nullable, but the result is guaranteed to non-null by Gatsby
blogPost(id: { eq: $id }) {
title
tags
}
}
This can be done with additional static analysis in plugin layer. But the proposal can be a simple solution.
Even if It's not clear in production cases that can customize schema, but it seems a huge advantage for ecosystem players using SDL and code-generation.
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.
Thank you for sharing another use case!
Addendum about 3rd party APIs
One general thought is that clients generally use GraphQL to fulfill at least two purposes in an application.
An example of the distinction is pagination:
The same distinction is relevant when considering the "required-ness" of a field: Individual components want to describe their data requirements - if a given component must have This proposal makes it difficult to use the same GraphQL to describe an individual component's data dependencies and to aggregate the dependencies for an entire user interface. Instead, the likely outcome of this proposal is that "sophisticated" clients (ie those that use AOT query compilation and also that use a client-side cache as a source of truth) would use
Considering this last point, it is likely that under this proposal many instances of
Imagine that the user interface is capable of rendering Header and Timeline independently: even if data for one is missing the application should render the section whose data is available. If
In practice, a client compiler would have to transform the query as follows:
In other words: client compilers would have to remove both instances of |
Cleanup after behavior was clarified
Co-authored-by: Wei Xue <[email protected]>
Thanks everyone for your comments! Earlier today the champions of this proposal attended a GraphQL Working Group meeting and officially made it to the first RFC stage. You can read the notes from the meeting here, and I'll update again when the recording of the presentation is online. We'll be moving this over to a PR in the main repo, and I'll try so summarize the existing comments as best I can. We definitely still want to address them. |
Creating this PR so folks can talk about the RFC and suggest additions and changes. If you make any changes to the document, feel free to add yourself to the top of the doc!
Rendered RFC here
Want to contribute? Here are some things we need:
We want to get feedback from a broad range of people since the impact of this RFC is somewhat large. People we would like additional feedback from: