Skip to content

Commit 0f277b1

Browse files
jwaldripclaude
andcommitted
feat: make @semanticNonNull directive opt-in
Move @semanticNonNull directive from prototype notation to a new opt-in module Absinthe.Type.BuiltIns.SemanticNonNull. Since @semanticNonNull is a proposed-spec feature (not yet finalized), users must now explicitly opt-in by adding: import_types Absinthe.Type.BuiltIns.SemanticNonNull to their schema definition. Also adds a new guide: guides/semantic-non-null.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ee16e35 commit 0f277b1

File tree

5 files changed

+218
-28
lines changed

5 files changed

+218
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
* **proposed-spec:** Add `@semanticNonNull` directive support ([#1406](https://github.com/absinthe-graphql/absinthe/pull/1406))
88
- **Note:** This directive is a proposed RFC and not yet part of the finalized GraphQL specification
9+
- **Opt-in required:** `import_types Absinthe.Type.BuiltIns.SemanticNonNull` in your schema
910
- Decouple nullability from error handling
1011
- `@semanticNonNull(levels: [Int])` directive with optional levels argument
1112
- Shorthand notation: `semantic_non_null: true` or `semantic_non_null: [0, 1]`

guides/semantic-non-null.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Semantic Non-Null
2+
3+
> **Note:** The `@semanticNonNull` directive is a proposed RFC from the [GraphQL Nullability Working Group](https://github.com/graphql/nullability-wg) and is not yet part of the finalized GraphQL specification. The implementation may change as the proposal evolves.
4+
5+
## Overview
6+
7+
The `@semanticNonNull` directive decouples nullability from error handling. It indicates that a field's resolver never intentionally returns null, but null may still be returned due to errors.
8+
9+
This allows clients to understand which fields may be null only due to errors versus fields that may intentionally be null.
10+
11+
## Enabling the Directive
12+
13+
Since `@semanticNonNull` is a proposed-spec feature, you must explicitly opt-in by importing the directive in your schema:
14+
15+
```elixir
16+
defmodule MyApp.Schema do
17+
use Absinthe.Schema
18+
19+
# Import the proposed-spec @semanticNonNull directive
20+
import_types Absinthe.Type.BuiltIns.SemanticNonNull
21+
22+
query do
23+
# ...
24+
end
25+
end
26+
```
27+
28+
Without this import, the `@semanticNonNull` directive will not be available in your schema.
29+
30+
## Basic Usage
31+
32+
### Using the Directive
33+
34+
Apply the directive to field definitions:
35+
36+
```elixir
37+
object :user do
38+
field :id, non_null(:id)
39+
40+
# This field is semantically non-null - it only returns null on errors
41+
field :name, :string do
42+
directive :semantic_non_null
43+
end
44+
45+
# This field may intentionally be null (no @semanticNonNull)
46+
field :nickname, :string
47+
end
48+
```
49+
50+
### Shorthand Notation
51+
52+
Absinthe provides a shorthand for applying `@semanticNonNull`:
53+
54+
```elixir
55+
object :user do
56+
# Using shorthand notation
57+
field :name, :string, semantic_non_null: true
58+
59+
# For list fields, specify levels
60+
field :posts, list_of(:post), semantic_non_null: [0, 1]
61+
end
62+
```
63+
64+
## The Levels Argument
65+
66+
The `levels` argument specifies which levels of the return type are semantically non-null:
67+
68+
- `[0]` (default) - The field itself is semantically non-null
69+
- `[1]` - For list fields, the list items are semantically non-null
70+
- `[0, 1]` - Both the field and its items are semantically non-null
71+
72+
### Examples
73+
74+
```elixir
75+
object :user do
76+
# The name field is semantically non-null
77+
field :name, :string, semantic_non_null: true # Same as [0]
78+
79+
# The posts list may be null, but items are semantically non-null
80+
field :posts, list_of(:post), semantic_non_null: [1]
81+
82+
# Both the friends list AND its items are semantically non-null
83+
field :friends, list_of(:user), semantic_non_null: [0, 1]
84+
end
85+
```
86+
87+
## Introspection
88+
89+
The directive adds introspection fields to `__Field`:
90+
91+
```graphql
92+
{
93+
__type(name: "User") {
94+
fields {
95+
name
96+
isSemanticNonNull
97+
semanticNonNullLevels
98+
}
99+
}
100+
}
101+
```
102+
103+
Response:
104+
105+
```json
106+
{
107+
"data": {
108+
"__type": {
109+
"fields": [
110+
{
111+
"name": "name",
112+
"isSemanticNonNull": true,
113+
"semanticNonNullLevels": [0]
114+
},
115+
{
116+
"name": "nickname",
117+
"isSemanticNonNull": false,
118+
"semanticNonNullLevels": null
119+
}
120+
]
121+
}
122+
}
123+
}
124+
```
125+
126+
## Client Considerations
127+
128+
Clients can use `@semanticNonNull` information to:
129+
130+
- Automatically throw errors when semantically non-null fields return null
131+
- Generate stricter types in code generation tools
132+
- Improve developer experience with better nullability handling
133+
134+
Apollo Client and other modern GraphQL clients are adding support for this directive. Consult your client's documentation for specific integration details.
135+
136+
## See Also
137+
138+
- [GraphQL Nullability Working Group](https://github.com/graphql/nullability-wg)
139+
- [Errors Guide](errors.md) for error handling in Absinthe

lib/absinthe/schema/prototype/notation.ex

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -62,34 +62,6 @@ defmodule Absinthe.Schema.Prototype.Notation do
6262
end)
6363
end
6464

65-
# https://github.com/graphql/nullability-wg
66-
directive :semantic_non_null do
67-
description """
68-
Indicates that a field is semantically non-null: the resolver never intentionally returns null,
69-
but null may still be returned due to errors.
70-
71-
This decouples nullability from error handling, allowing clients to understand which fields
72-
may be null only due to errors versus fields that may intentionally be null.
73-
"""
74-
75-
arg :levels, non_null(list_of(non_null(:integer))),
76-
default_value: [0],
77-
description: """
78-
Specifies which levels of the return type are semantically non-null.
79-
- [0] means the field itself is semantically non-null
80-
- [1] for list fields means the list items are semantically non-null
81-
- [0, 1] means both the field and its items are semantically non-null
82-
"""
83-
84-
repeatable false
85-
on [:field_definition]
86-
87-
expand(fn args, node ->
88-
levels = Map.get(args, :levels, [0])
89-
%{node | __private__: Keyword.put(node.__private__, :semantic_non_null, levels)}
90-
end)
91-
end
92-
9365
def pipeline(pipeline) do
9466
pipeline
9567
|> Absinthe.Pipeline.without(Absinthe.Phase.Schema.Validation.QueryTypeMustBeObject)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
defmodule Absinthe.Type.BuiltIns.SemanticNonNull do
2+
@moduledoc """
3+
Proposed-spec @semanticNonNull directive.
4+
5+
This directive is part of the [GraphQL Nullability Working Group](https://github.com/graphql/nullability-wg)
6+
proposal and is not yet part of the finalized GraphQL specification.
7+
8+
## Usage
9+
10+
To enable @semanticNonNull in your schema, import this module:
11+
12+
defmodule MyApp.Schema do
13+
use Absinthe.Schema
14+
15+
import_types Absinthe.Type.BuiltIns.SemanticNonNull
16+
17+
query do
18+
# ...
19+
end
20+
end
21+
22+
Then you can use the directive on field definitions:
23+
24+
object :user do
25+
field :id, non_null(:id)
26+
27+
# This field is semantically non-null - it only returns null on errors
28+
field :name, :string do
29+
directive :semantic_non_null
30+
end
31+
end
32+
33+
## Purpose
34+
35+
The @semanticNonNull directive decouples nullability from error handling. It indicates
36+
that a field's resolver never intentionally returns null, but null may still be returned
37+
due to errors. This allows clients to understand which fields may be null only due to
38+
errors versus fields that may intentionally be null.
39+
40+
## Arguments
41+
42+
- `levels` - Specifies which levels of the return type are semantically non-null:
43+
- `[0]` (default) - The field itself is semantically non-null
44+
- `[1]` - For list fields, the list items are semantically non-null
45+
- `[0, 1]` - Both the field and its items are semantically non-null
46+
"""
47+
48+
use Absinthe.Schema.Notation
49+
50+
directive :semantic_non_null do
51+
description """
52+
Indicates that a field is semantically non-null: the resolver never intentionally returns null,
53+
but null may still be returned due to errors.
54+
55+
This decouples nullability from error handling, allowing clients to understand which fields
56+
may be null only due to errors versus fields that may intentionally be null.
57+
"""
58+
59+
arg :levels, non_null(list_of(non_null(:integer))),
60+
default_value: [0],
61+
description: """
62+
Specifies which levels of the return type are semantically non-null.
63+
- [0] means the field itself is semantically non-null
64+
- [1] for list fields means the list items are semantically non-null
65+
- [0, 1] means both the field and its items are semantically non-null
66+
"""
67+
68+
repeatable false
69+
on [:field_definition]
70+
71+
expand(fn args, node ->
72+
levels = Map.get(args, :levels, [0])
73+
%{node | __private__: Keyword.put(node.__private__, :semantic_non_null, levels)}
74+
end)
75+
end
76+
end

test/absinthe/type/semantic_nullability_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ defmodule Absinthe.Type.SemanticNullabilityTest do
66
defmodule TestSchema do
77
use Absinthe.Schema
88

9+
import_types Absinthe.Type.BuiltIns.SemanticNonNull
10+
911
object :post do
1012
field :id, :id
1113
field :title, :string

0 commit comments

Comments
 (0)