|
| 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 value — the 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 | +``` |
0 commit comments