Skip to content

Commit e975ecc

Browse files
authored
Update CCN RFC, and introduce a decision log (#966)
* list syntax decision detailed * loosened restrictions described * more decision logs * more decision logs * null propagation rules updates * reduced max line length * spelling fixes
1 parent 90f0665 commit e975ecc

File tree

1 file changed

+144
-28
lines changed

1 file changed

+144
-28
lines changed

rfcs/ClientControlledNullability.md

+144-28
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ nullability of fields for individual operations.
1515

1616
## Definitions
1717

18-
- **Required field** - A field which is modified with `!` such that a non-null value is required on
19-
a Nullable or Non-Nullable type.
18+
- **Required field** - A field which is marked with `!`.
2019

21-
- **Optional field** - A field which is modified with `?` such that a null value is allowed on a
22-
Non-Nullable or Nullable type.
20+
- **Optional field** - A field which is marked with `?`.
2321

2422
## 📜 Problem Statement
2523

@@ -36,11 +34,10 @@ thrown. Beyond
3634
a request can
3735
> have different authorization rules.
3836
39-
The problem with the SDL Non-Nullable (!) is that it eliminates the possibility of partial failure
37+
The problem with the SDL Non-Nullable (`!`) is that it eliminates the possibility of partial failure
4038
on a given type. This forces schema authors to decide for which fields partial failure is
41-
acceptable. A GraphQL schema author
42-
may not be in the best position to predict whether partial failure will be acceptable or
43-
unacceptable for every canvas that makes use of a field.
39+
acceptable. A GraphQL schema author may not be in the best position to predict whether partial failure
40+
will be acceptable or unacceptable for every canvas that makes use of a field.
4441

4542
While the schema can have nullable fields for valid reasons (such as federation), in some cases the
4643
client wants to decide if it accepts a `null` value for the result to simplify the client-side
@@ -56,34 +53,31 @@ Each client controlled nullability designator overrides the schema-defined nulla
5653
it's attached to for the duration of the operation.
5754

5855
### `!`
59-
The proposed client-controlled required designator would have identical semantics to the current
60-
schema-defined
61-
[Non-Null](https://spec.graphql.org/draft/#sec-Executing-Selection-Sets.Errors-and-Non-Null-Fields).
62-
Specifically:
63-
64-
- If during ExecuteSelectionSet() a field **designated required by the operation or** with a
65-
non-null fieldType raises a field error then that error must propagate to this entire selection
66-
set, either resolving to null if allowed or further propagated to a parent field.
56+
The proposed client-controlled required designator would have similar, but not identical semantics to the current
57+
schema-defined [Non-Null](https://spec.graphql.org/draft/#sec-Executing-Selection-Sets.Errors-and-Non-Null-Fields).
58+
Specifically if a required field resolves to `null`, then `null` propagation extends to the nearest optional
59+
parent rather than the nearest nullable parent. In the event that no optional parent exists, the `data` field
60+
of the response will be `null`.
6761

6862
### `?`
6963
The proposed client-controlled optional designator would have identical semantics to the current
70-
schema-defined default behavior. Fields that resolve to `null` return `null` for that field with no
71-
additional side effects.
64+
schema-defined default behavior. Fields that resolve to `null` return `null` for that field. Additionally,
65+
fields marked with `?` act as a stopping point for `null` propagation caused by required fields.
7266

7367
## ✅ Validation
7468

7569
If a developer executed an operation with two fields name `foo`, one a `String` and the other an
7670
`Int`, the operation would be declared invalid by the server. The same is true if one of the fields
77-
is designated required but both are otherwise the same type. In this example, `someValue` could be
71+
is designated required but both are otherwise the same type. In this example, `nickname` could be
7872
either a `String` or a `String!` which are two different types and therefor can not be merged:
7973

8074
```graphql
8175
fragment conflictingDifferingResponses on Pet {
8276
... on Dog {
83-
someValue: nickname
77+
nickname
8478
}
8579
... on Cat {
86-
someValue: nickname!
80+
nickname!
8781
}
8882
}
8983
```
@@ -94,7 +88,7 @@ The client can express that a schema field is required by using the `!` syntax i
9488
definition:
9589
```graphql
9690
query GetBusinessName($id: String!) {
97-
business(id: $id) {
91+
business(id: $id)? {
9892
name!
9993
}
10094
}
@@ -120,7 +114,7 @@ requirements for a particular feature. For example, using a GraphQL client like
120114
mobile, the following query
121115
```graphql
122116
query GetBusinessName($id: String!) {
123-
business(id: $id) {
117+
business(id: $id)? {
124118
name!
125119
}
126120
}
@@ -143,7 +137,7 @@ is more ergonomic to use since the developer does not need to unwrap the value e
143137
accessed.
144138

145139
### 3rd-party GraphQL APIs
146-
Marking field Non-Nullable in schema is not possible in every use case. For example, when a
140+
Marking a field Non-Nullable in schema is not possible in every use case. For example, when a
147141
developer is using a 3rd-party API such as
148142
[Github's GraphQL API](https://docs.github.com/en/graphql) they won't be able to alter Github's
149143
schema, but they may still want to have certain fields be required in their application. Even within
@@ -246,7 +240,7 @@ field’s ancestors to find the next nullable field. In the following GraphQL re
246240
}
247241
```
248242

249-
If isStarred is Non-Nullable but returns `null` and business is nullable, the result will be:
243+
If `isStarred` is Non-Nullable but resolves to `null` and business is nullable, the result will be:
250244

251245
```json
252246
{
@@ -256,15 +250,15 @@ If isStarred is Non-Nullable but returns `null` and business is nullable, the re
256250
}
257251
```
258252

259-
Even if `name` returns valid results, the response would no longer provide this data. If business is
253+
Even if `isStarred` resolves to a valid result, the response would no longer provide this data. If business is
260254
Non-Nullable, the response will be:
261255
```json
262256
{
263257
"data": null
264258
}
265259
```
266260

267-
In the case that the service storing user stars is unavailable, the UI may want to go ahead and
261+
In the case that the service storing business stars is unavailable, the UI may want to go ahead and
268262
render the component without a star (effectively defaulting `isStarred` to `false`). A Non-Nullable
269263
field in the schema makes it impossible for the client to receive partial results from the server,
270264
and thus potentially forces the entire component to fail to render.
@@ -282,4 +276,126 @@ generation.
282276
### Alternatives to `!`
283277
#### `!!`
284278
This would follow the precedent set by Kotlin. It's more verbose and diverges from GraphQL's SDL
285-
precedent.
279+
precedent.
280+
281+
## Decision Log
282+
283+
This proposal started out with a very simple premise and implementation, and has gotten more complex as
284+
the community has explored edge cases and facets about how GraphQL is actually used in practice. For
285+
example this proposal starts out by talking about accommodating the "best practices" that are recommended
286+
by the GraphQL documentation and the community, but we discovered pretty early on that there are
287+
legitimate use cases where the "best practices" are rightfully ignored. Some of those use cases are
288+
covered in "`?` as a counterpart to `!`".
289+
290+
In order to cover instances like that, we've needed to justify additional complexity which can be
291+
difficult to understand for newcomers without (at this point a full year) of context. This decision
292+
log was written with newcomers in mind to avoid rediscussing issues that have already been hashed out,
293+
and to make it easier to understand why certain decisions have been made. At the time of writing,
294+
the decisions here aren't set in stone, so any future discussions can use this log as a starting point.
295+
296+
### `?` as a counterpart to `!`
297+
298+
Lee was the first person [to suggest](https://github.com/graphql/graphql-spec/issues/867#issuecomment-840807186)
299+
that the inverse of `!` should exist and that it should be represented by `?`. The
300+
[reasoning](https://github.com/graphql/graphql-spec/issues/867#issuecomment-841372320) was that it "completes
301+
the story of control" and provides a guaranteed stopping point for `null` propagation if we're using the existing
302+
`null` propagation rules. The feeling was that "introducing ! without ? is like introducing `throw` without `catch`".
303+
304+
Lee also surfaced that there are some use cases like his own at Robinhood where they're trying to balance
305+
developer experience and data preservation, and have opted to mark quite a few fields `Non-Null`. Data
306+
preservation is very important because Robinhood is working with financial data, so they have the opposite
307+
problem where they sometimes want to be able to halt `null` propagation, rather than the inverse use case which
308+
this proposal originally supported.
309+
310+
Developers from Apollo indicated that many of their customers face problems around schema breaking where the
311+
solution to developer experience gripes is to make a breaking change and swap a field from nullable to `Non-Nullable`
312+
or vice versa, which can be a labor intensive process.
313+
314+
Since there seemed to be general consensus that `?` was a good addition to the proposal, it was adopted without a vote.
315+
316+
Subsequently there was discussion around whether `?` could be introduced in a later proposal, and there was general
317+
agreement that the usability of `!` is limited without `?`, and the selected `null` propagation behavior described
318+
below solidifies the decision to introduce both additions in a single proposal.
319+
320+
### List syntax
321+
Developers from Apollo [suggested](https://github.com/graphql/graphql-spec/pull/895#issuecomment-961442966) early
322+
on that users would want to apply CCN syntax to list elements. The possibility had been suggested earlier than
323+
that as well, but it was put off because neither Netflix nor Relay's CCN counterparts had the feature, and it
324+
hadn't been a problem yet. However there was enough interest during community feedback sessions to adopt it
325+
into the proposal. [Discussions](https://github.com/graphql/graphql-wg/discussions/864) around which specific
326+
syntax to adopt happened over the following months.
327+
328+
Options other than the one that was landed on included the following:
329+
330+
```graphql
331+
twoDimensionalList!!?
332+
```
333+
The folks that voted against this option felt that it was unclear how it should be interpreted, whether operators
334+
should be applied from the outside-in, or inside-out.
335+
336+
```graphql
337+
twoDimensionalList as [[Int!]!]
338+
twoDimensionalList <= [[Int!]!]
339+
```
340+
The folks that voted against this option felt that this option read like a type-cast, and that the inclusion
341+
of a type placed an undue burden on client developers. Validation would fail if the type was incorrect, and
342+
didn't provide much additional value.
343+
344+
```graphql
345+
twoDimensionalList[[!]!]?
346+
```
347+
This syntax, called "the bracket syntax" during discussions was selected for adoption by majority vote at the
348+
[March 3rd, 2022 GraphQL Working Group Meeting](https://github.com/graphql/graphql-wg/blob/main/agendas/2022/2022-03-03.md).
349+
9 out of 10 participants voted for this option with the final vote going to the `<= [[Int!]!]` option.
350+
351+
Initially there was a restriction on the bracket syntax where the depth of the syntax needed to match depth of
352+
the field's list type, but participants at the same meeting felt that restriction should be loosened so
353+
that developers could opt to apply the syntax to only the field itself and ignore the elements of the
354+
list. Under that new rule the following would also be valid, and the two examples would be equivalent.
355+
356+
```graphql
357+
twoDimensionalList!
358+
twoDimensionalList[[]]!
359+
```
360+
361+
There are however some open concerns that the first of the two examples could be ambiguous as to whether
362+
the `!` applies to the field as a whole or to the list elements.
363+
364+
### `!` propagates `null` to nearest `?` rather than nearest nullable field
365+
The selected mechanics were most requested by the folks at Meta working on [Relay](https://relay.dev/).
366+
Relay wanted this behavior for a few reasons
367+
- Relay presents a facade of fragment isolation for its own
368+
[`@required` directive](https://relay.dev/docs/next/guides/required-directive/). If a field is `null`,
369+
rather than merging fragments and propagating `null` to all sibling fields on selection sets that use
370+
that fragment, the most popular option as chosen by ~90% of developers is to have the request throw
371+
and utilize React's error boundaries. It also has the option to do `null` propagation, but that too
372+
is bound to a single fragment. Because of this, it likely won't be able to use CCN at first, but
373+
developers would like to be able to use it in the future once
374+
[Fragment Modularity](https://github.com/graphql/graphql-wg/blob/main/rfcs/FragmentModularity.md)
375+
makes it into the spec. The selected option preserves the possibility of Relay and other clients that utilize
376+
fragments heavily using CCN in the future.
377+
- The throwing option that Relay provides on their `@required` directive effectively allows developers
378+
to indicate "If some required field is missing, throw out everything from this field to the fragment boundary"
379+
which is tighter client control than was offered by the initial iteration of this proposal where `!` only
380+
transformed a field into a `Non-Null`. In that case, the server still had control over where `null`
381+
propagation halted with which fields the schema said were nullable. The selected `null` propagation option
382+
is slightly closer to the most popular `@required` option in that way.
383+
384+
With the initial iteration of this proposal if users wanted to guarantee that all fields through multiple
385+
parents should be lost in the event that a child is `null`, they would need to mark each level with a `!`,
386+
but the selected option avoids that.
387+
388+
With the selected option, forgetting to include a `?` is potentially dangerous because it would result
389+
in more fields being lost than intended, all the way up to the `data` field in the worst case scenario.
390+
There were concerns that that was a blocker, but arguments were made that because most queries are relatively
391+
small, it wasn't actually that dangerous. Clients have also often been treating the existence of any errors
392+
as a failed request and thrown out entire responses, so in effect, clients been choosing the "worst case
393+
scenario" when given the option.
394+
395+
The behavior where `!` propagates `null` to nearest `?` was selected for adoption by majority vote at the
396+
[March 3rd, 2022 GraphQL Working Group Meeting](https://github.com/graphql/graphql-wg/blob/main/agendas/2022/2022-03-03.md).
397+
7 out of 8 participants voted for this option with the final vote going to behavior where the `!` would be non-destructive.
398+
399+
The non-destructive option was turned down because having different behavior per-client wasn't desirable, and
400+
it provided no benefits to naive clients (like a bash script) because extra processing would be required
401+
for it to be a value-add.

0 commit comments

Comments
 (0)