GAP-49: @semanticNonNull Directive
Version: Draft
Authors: Martin Bonnin
Discussion: github.com/graphql/gaps/pull/49

@semanticNonNull Directive Specification

1Introduction

This document specifies the @semanticNonNull directive, a schema directive that indicates a field position is semantically non null: it is only null when there is a matching error in the response errors array, and is otherwise guaranteed to contain a value.

GraphQL’s Non-Null type (!) serves two purposes at once: it asserts that a position can never logically be null, and it dictates that, when an error occurs in that position during execution, the resulting null is propagated to the nearest nullable parent. Because error propagation can erase large amounts of otherwise-valid data, API authors frequently make fields nullable purely to contain the blast radius of field errors — even when those fields are, in the absence of an error, always present.

@semanticNonNull lets a schema separate these two concerns. A field may remain nullable on the wire (so that an error in it does not propagate) while still declaring that, absent a matching error, the position always contains a value.

Example
Example № 1type User {
  # `name` is nullable on the wire, but is only ever null if `name` itself
  # produced an error. Tooling may treat it as non-null otherwise.
  name: String @semanticNonNull

  # The list and each of its elements are semantically non null.
  friends: [User] @semanticNonNull(levels: [0, 1])
}
Use Cases
  • Code generators may emit non-null types for semantically non-null positions when the client handles field errors through an out-of-band mechanism (such as a result type or an exception raised at the edge of the selection set), sparing developers from null checks on positions that are only ever null on error.
  • Schema authors may keep fields nullable for resilience (so that field errors do not propagate and erase sibling data) without sacrificing the ability to communicate that those fields are, in practice, always present.
Relationship to `onError`

The onError proposal allows a client to opt out of error propagation (onError: "NULL"), so that a field error produces a localized null rather than propagating up to the nearest nullable parent. Once error propagation is no longer a concern, the Non-Null type (!) can be used to mark every position where null is not a semantically valid value — the true nullability of the schema.

Representing that nullability with ! is not always possible for existing services, however. Adding ! to an existing field may increase the blast radius of an error for clients that have not opted out of error propagation.

@semanticNonNull addresses this. Because it is additive metadata that does not change the wire type, a nullability-aware client may read the true nullability of a field and omit its non-null checks, while clients that are unaware of the directive observe the field exactly as before. onError and @semanticNonNull describe the same underlying truth — which positions are truly non-null — but @semanticNonNull can be adopted incrementally without affecting existing clients.

2Definition

directive @semanticNonNull(levels: [Int!]! = [0]) on FIELD_DEFINITION

The @semanticNonNull directive indicates that a field’s result is semantically non null at the indicated levels.

A position is semantically non null if it is only null when there is a matching error in the errors array of the response. In all other cases, the position contains a value.

@semanticNonNull may only be applied to a FieldDefinition whose type is nullable at the indicated levels. Applying it to a position that is already a Non-Null type at that level is meaningless (the position is already guaranteed non-null by the type system) and must be considered an error.

3The levels argument

The levels argument selects which levels of a (possibly nested) List type are semantically non null. Levels are zero-indexed, where level 0 is the outermost position (the field’s own value).

The default value of [0] makes only the outermost position semantically non null, matching the common case of a non-list field.

A level that is negative, or that exceeds the list dimensionality of the annotated field, must be considered an error.

Examples

Given the field type "[[String]]":

Application Semantically non-null positions
@semanticNonNull the outer list
@semanticNonNull(levels: [1]) each inner list
@semanticNonNull(levels: [2]) each String element
@semanticNonNull(levels: [0, 1, 2]) the outer list, each inner list, and each String
Example № 2type Query {
  # The list itself is semantically non null; individual elements may be null.
  tags: [String] @semanticNonNull

  # Both the list and each element are semantically non null.
  ids: [ID] @semanticNonNull(levels: [0, 1])
}

§Index

  1. semantically non null
  1. 1Introduction
  2. 2Definition
  3. 3The levels argument
  4. §Index