Skip to content

Commit c0b1b8c

Browse files
authored
Add @semanticNonNull draft (#49)
I currently use https://specs.apollo.dev/nullability/v0.4/#@semanticNonNull as reference but GAPs feel like a better place. Also, contrary to `onError`, I don't necessarily expect that `@semanticNonNull` makes it to the main spec since it's mainly used for compatibility. Hence me opening that GAP. Let me know what you think!
2 parents be2df21 + 4d7594a commit c0b1b8c

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

gaps/GAP-49/DRAFT.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# @semanticNonNull Directive Specification
2+
3+
## Introduction
4+
5+
:: This document specifies the `@semanticNonNull` directive, a schema directive
6+
that indicates a field position is _semantically non null_: it is only null when
7+
there is a matching error in the response `errors` array, and is otherwise
8+
guaranteed to contain a value.
9+
10+
GraphQL's Non-Null type (`!`) serves two purposes at once: it asserts that a
11+
position can never logically be null, and it dictates that, when an error occurs
12+
in that position during execution, the resulting null is _propagated_ to the
13+
nearest nullable parent. Because error propagation can erase large amounts of
14+
otherwise-valid data, API authors frequently make fields nullable purely to
15+
contain the blast radius of field errors — even when those fields are, in the
16+
absence of an error, always present.
17+
18+
`@semanticNonNull` lets a schema separate these two concerns. A field may remain
19+
nullable on the wire (so that an error in it does not propagate) while still
20+
declaring that, absent a matching error, the position always contains a value.
21+
22+
**Example**
23+
24+
```graphql example
25+
type User {
26+
# `name` is nullable on the wire, but is only ever null if `name` itself
27+
# produced an error. Tooling may treat it as non-null otherwise.
28+
name: String @semanticNonNull
29+
30+
# The list and each of its elements are semantically non null.
31+
friends: [User] @semanticNonNull(levels: [0, 1])
32+
}
33+
```
34+
35+
**Use Cases**
36+
37+
- Code generators may emit non-null types for semantically non-null positions
38+
when the client handles field errors through an out-of-band mechanism (such as
39+
a result type or an exception raised at the edge of the selection set), sparing
40+
developers from null checks on positions that are only ever null on error.
41+
- Schema authors may keep fields nullable for resilience (so that field errors
42+
do not propagate and erase sibling data) without sacrificing the ability to
43+
communicate that those fields are, in practice, always present.
44+
45+
**Relationship to `onError`**
46+
47+
The [`onError` proposal](https://github.com/graphql/graphql-spec/pull/1163)
48+
allows a client to opt out of error propagation (`onError: "NULL"`), so that a field error produces
49+
a localized {null} rather than propagating up to the nearest nullable parent. Once
50+
error propagation is no longer a concern, the _Non-Null_ type (`!`) can be used
51+
to mark every position where {null} is not a semantically valid valuethe true
52+
nullability of the schema.
53+
54+
Representing that nullability with `!` is not always possible for existing services, however. Adding
55+
`!` to an existing field may increase the blast radius of an error for clients that have not opted
56+
out of error propagation.
57+
58+
`@semanticNonNull` addresses this. Because it is additive metadata that does not
59+
change the wire type, a nullability-aware client may read the true nullability of
60+
a field and omit its non-null checks, while clients that are unaware of the
61+
directive observe the field exactly as before. `onError` and `@semanticNonNull`
62+
describe the same underlying truth — which positions are truly non-null — but
63+
`@semanticNonNull` can be adopted incrementally without affecting existing
64+
clients.
65+
66+
## Definition
67+
68+
```graphql
69+
directive @semanticNonNull(levels: [Int!]! = [0]) on FIELD_DEFINITION
70+
```
71+
72+
The `@semanticNonNull` directive indicates that a field's result is
73+
_semantically non null_ at the indicated _levels_.
74+
75+
A position is _semantically non null_ if it is only {null} when there is a
76+
matching error in the `errors` array of the response. In all other cases, the
77+
position contains a value.
78+
79+
`@semanticNonNull` may only be applied to a _FieldDefinition_ whose type is
80+
nullable at the indicated _levels_. Applying it to a position that is already a
81+
_Non-Null_ type at that level is meaningless (the position is already
82+
guaranteed non-null by the type system) and must be considered an error.
83+
84+
## The levels argument
85+
86+
The _levels_ argument selects which _levels_ of a (possibly nested) _List_ type
87+
are semantically non null. Levels are zero-indexed, where level {0} is the
88+
outermost position (the field's own value).
89+
90+
- For a non-list type, only level {0} is meaningful, and it refers to the
91+
field's value itself.
92+
- For a _List_ type, level {0} refers to the list itself, level {1} refers to
93+
each element of the list, level {2} to each element of each nested list, and
94+
so on.
95+
96+
The default value of `[0]` makes only the outermost position semantically non
97+
null, matching the common case of a non-list field.
98+
99+
A _level_ that is negative, or that exceeds the list dimensionality of the
100+
annotated field, must be considered an error.
101+
102+
**Examples**
103+
104+
Given the field type {"[[String]]"}:
105+
106+
| Application | Semantically non-null positions |
107+
| ------------------------------------- | -------------------------------------------------- |
108+
| `@semanticNonNull` | the outer list |
109+
| `@semanticNonNull(levels: [1])` | each inner list |
110+
| `@semanticNonNull(levels: [2])` | each {String} element |
111+
| `@semanticNonNull(levels: [0, 1, 2])` | the outer list, each inner list, and each {String} |
112+
113+
```graphql example
114+
type Query {
115+
# The list itself is semantically non null; individual elements may be null.
116+
tags: [String] @semanticNonNull
117+
118+
# Both the list and each element are semantically non null.
119+
ids: [ID] @semanticNonNull(levels: [0, 1])
120+
}
121+
```

gaps/GAP-49/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# GAP-49: @semanticNonNull Directive
2+
3+
## Overview
4+
5+
This proposal defines the `@semanticNonNull` directive, a schema directive that
6+
marks a field position as _semantically non null_: the position is guaranteed to
7+
contain a value unless there is a matching error in the response's `errors`
8+
array.
9+
10+
This separates two concepts that GraphQL's `!` (Non-Null) type conflates:
11+
12+
- **"this position can never logically be null"** (semantic nullability), and
13+
- **"this position becomes null when the field errors"** (error nullability).
14+
15+
By expressing semantic nullability separately, code generation tools can emit
16+
non-null types for clients that can opt-out of error propagation with `onError: NULL`, removing the unnecessary null checks that
17+
clients would otherwise have to write.
18+
19+
## Relationship to prior art
20+
21+
This directive is part of the broader
22+
[GraphQL Nullability specification](https://specs.apollo.dev/nullability/) being
23+
developed in the
24+
[GraphQL Nullability Working Group](https://github.com/graphql/nullability-wg).
25+
The full nullability specification also defines client-side directives
26+
(`@catch`, `@catchByDefault`) describing how clients handle field errors. This
27+
GAP scopes itself to the schema-side `@semanticNonNull` (and the companion
28+
`@semanticNonNullField`) directive, which is independently useful and can be
29+
adopted on its own.
30+
31+
Related discussions and prior art:
32+
33+
- [GraphQL Nullability WG](https://github.com/graphql/nullability-wg)
34+
- [graphql-spec #1452 — "Client Controlled Nullability"](https://github.com/graphql/graphql-spec/pull/1452)
35+
and the broader nullability discussions in the GraphQL specification.
36+
- [`specs.apollo.dev/nullability/v0.4`](https://specs.apollo.dev/nullability/v0.4/#@semanticNonNull).

gaps/GAP-49/metadata.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
id: 49
2+
title: "@semanticNonNull Directive"
3+
summary: >
4+
A schema directive that marks a field position as "semantically non null" —
5+
guaranteed to be non-null unless there is a matching error in the response
6+
`errors` array — allowing tooling to generate non-null types for newer clients
7+
without breaking older clients that cannot enable `onError: "NULL"`.
8+
status: proposal
9+
authors:
10+
- name: "Martin Bonnin"
11+
email: "martin@apollographql.com"
12+
githubUsername: "@martinbonnin"
13+
sponsor: "@martinbonnin"
14+
discussion: "https://github.com/graphql/gaps/pull/49"

0 commit comments

Comments
 (0)