From 17461b9b7c9124bcccca1282570a8b5f166b9c12 Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Sun, 2 Feb 2025 08:45:34 +0100
Subject: [PATCH 01/10] Implement first version

Co-Authored-By: Benjie <code@benjiegillam.com>
Co-Authored-By: twof <hello@alex.dev>
---
 src/__tests__/starWarsIntrospection-test.ts   |   1 +
 src/execution/__tests__/executor-test.ts      |   2 +
 .../__tests__/semantic-nullability-test.ts    | 220 ++++++++++++++++++
 src/execution/execute.ts                      |  44 ++++
 src/graphql.ts                                |   8 +
 src/index.ts                                  |   6 +
 src/language/__tests__/parser-test.ts         |  64 +++++
 src/language/__tests__/predicates-test.ts     |   1 +
 src/language/__tests__/schema-printer-test.ts |  29 ++-
 src/language/ast.ts                           |  24 +-
 src/language/index.ts                         |   1 +
 src/language/kinds.ts                         |   2 +
 src/language/lexer.ts                         |   5 +-
 src/language/parser.ts                        |  24 ++
 src/language/predicates.ts                    |   4 +-
 src/language/printer.ts                       |   9 +
 src/language/tokenKind.ts                     |   1 +
 src/type/__tests__/introspection-test.ts      |  54 ++++-
 src/type/__tests__/predicate-test.ts          |  66 +++++-
 src/type/__tests__/schema-test.ts             |   1 +
 src/type/definition.ts                        | 206 +++++++++++++++-
 src/type/index.ts                             |   4 +
 src/type/introspection.ts                     |  97 +++++++-
 src/utilities/__tests__/printSchema-test.ts   |  22 +-
 src/utilities/buildClientSchema.ts            |   9 +
 src/utilities/extendSchema.ts                 |  13 ++
 src/utilities/findBreakingChanges.ts          |  21 ++
 src/utilities/getIntrospectionQuery.ts        |  25 +-
 src/utilities/index.ts                        |   1 +
 src/utilities/lexicographicSortSchema.ts      |   5 +
 src/utilities/typeComparators.ts              |  19 +-
 src/utilities/typeFromAST.ts                  |  10 +-
 .../rules/OverlappingFieldsCanBeMergedRule.ts |   9 +
 33 files changed, 984 insertions(+), 23 deletions(-)
 create mode 100644 src/execution/__tests__/semantic-nullability-test.ts

diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts
index d637787c4a..0dc95f0a7e 100644
--- a/src/__tests__/starWarsIntrospection-test.ts
+++ b/src/__tests__/starWarsIntrospection-test.ts
@@ -42,6 +42,7 @@ describe('Star Wars Introspection Tests', () => {
             { name: '__TypeKind' },
             { name: '__Field' },
             { name: '__InputValue' },
+            { name: '__TypeNullability' },
             { name: '__EnumValue' },
             { name: '__Directive' },
             { name: '__DirectiveLocation' },
diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts
index c758d3e426..afc73d5c08 100644
--- a/src/execution/__tests__/executor-test.ts
+++ b/src/execution/__tests__/executor-test.ts
@@ -263,6 +263,7 @@ describe('Execute: Handles basic execution tasks', () => {
       'rootValue',
       'operation',
       'variableValues',
+      'errorPropagation',
     );
 
     const operation = document.definitions[0];
@@ -275,6 +276,7 @@ describe('Execute: Handles basic execution tasks', () => {
       schema,
       rootValue,
       operation,
+      errorPropagation: true
     });
 
     const field = operation.selectionSet.selections[0];
diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts
new file mode 100644
index 0000000000..20a33c2ffa
--- /dev/null
+++ b/src/execution/__tests__/semantic-nullability-test.ts
@@ -0,0 +1,220 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { GraphQLError } from '../../error/GraphQLError';
+
+import type { ExecutableDefinitionNode, FieldNode } from '../../language/ast';
+import { parse } from '../../language/parser';
+
+import {
+  GraphQLNonNull,
+  GraphQLObjectType,
+  GraphQLSemanticNonNull,
+  GraphQLSemanticNullable,
+} from '../../type/definition';
+import { GraphQLString } from '../../type/scalars';
+import { GraphQLSchema } from '../../type/schema';
+
+import { execute } from '../execute';
+
+describe('Execute: Handles Semantic Nullability', () => {
+  const DeepDataType = new GraphQLObjectType({
+    name: 'DeepDataType',
+    fields: {
+      f: { type: new GraphQLNonNull(GraphQLString) },
+    },
+  });
+
+  const DataType: GraphQLObjectType = new GraphQLObjectType({
+    name: 'DataType',
+    fields: () => ({
+      a: { type: new GraphQLSemanticNullable(GraphQLString) },
+      b: { type: new GraphQLSemanticNonNull(GraphQLString) },
+      c: { type: new GraphQLNonNull(GraphQLString) },
+      d: { type: new GraphQLSemanticNonNull(DeepDataType) },
+    }),
+  });
+
+  it('SemanticNonNull throws error on null without error', async () => {
+    const data = {
+      a: () => 'Apple',
+      b: () => null,
+      c: () => 'Cookie',
+    };
+
+    const document = parse(`
+        query {
+          b
+        }
+      `);
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    const executable = document.definitions?.values().next()
+      .value as ExecutableDefinitionNode;
+    const selectionSet = executable.selectionSet.selections
+      .values()
+      .next().value;
+
+    expect(result).to.deep.equal({
+      data: {
+        b: null,
+      },
+      errors: [
+        new GraphQLError(
+          'Cannot return null for semantic-non-nullable field DataType.b.',
+          {
+            nodes: selectionSet,
+            path: ['b'],
+          },
+        ),
+      ],
+    });
+  });
+
+  it('SemanticNonNull succeeds on null with error', async () => {
+    const data = {
+      a: () => 'Apple',
+      b: () => {
+        throw new Error('Something went wrong');
+      },
+      c: () => 'Cookie',
+    };
+
+    const document = parse(`
+        query {
+          b
+        }
+      `);
+
+    const executable = document.definitions?.values().next()
+      .value as ExecutableDefinitionNode;
+    const selectionSet = executable.selectionSet.selections
+      .values()
+      .next().value;
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    expect(result).to.deep.equal({
+      data: {
+        b: null,
+      },
+      errors: [
+        new GraphQLError('Something went wrong', {
+          nodes: selectionSet,
+          path: ['b'],
+        }),
+      ],
+    });
+  });
+
+  it('SemanticNonNull halts null propagation', async () => {
+    const deepData = {
+      f: () => null,
+    };
+
+    const data = {
+      a: () => 'Apple',
+      b: () => null,
+      c: () => 'Cookie',
+      d: () => deepData,
+    };
+
+    const document = parse(`
+        query {
+          d {
+            f
+          }
+        }
+      `);
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    const executable = document.definitions?.values().next()
+      .value as ExecutableDefinitionNode;
+    const dSelectionSet = executable.selectionSet.selections.values().next()
+      .value as FieldNode;
+    const fSelectionSet = dSelectionSet.selectionSet?.selections
+      .values()
+      .next().value;
+
+    expect(result).to.deep.equal({
+      data: {
+        d: null,
+      },
+      errors: [
+        new GraphQLError(
+          'Cannot return null for non-nullable field DeepDataType.f.',
+          {
+            nodes: fSelectionSet,
+            path: ['d', 'f'],
+          },
+        ),
+      ],
+    });
+  });
+
+  it('SemanticNullable allows null values', async () => {
+    const data = {
+      a: () => null,
+      b: () => null,
+      c: () => 'Cookie',
+    };
+
+    const document = parse(`
+        query {
+          a
+        }
+      `);
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    expect(result).to.deep.equal({
+      data: {
+        a: null,
+      },
+    });
+  });
+
+  it('SemanticNullable allows non-null values', async () => {
+    const data = {
+      a: () => 'Apple',
+      b: () => null,
+      c: () => 'Cookie',
+    };
+
+    const document = parse(`
+        query {
+          a
+        }
+      `);
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    expect(result).to.deep.equal({
+      data: {
+        a: 'Apple',
+      },
+    });
+  });
+});
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 55c22ea9de..33aa1dd6f1 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -43,6 +43,8 @@ import {
   isListType,
   isNonNullType,
   isObjectType,
+  isSemanticNonNullType,
+  isSemanticNullableType,
 } from '../type/definition';
 import {
   SchemaMetaFieldDef,
@@ -115,6 +117,7 @@ export interface ExecutionContext {
   typeResolver: GraphQLTypeResolver<any, any>;
   subscribeFieldResolver: GraphQLFieldResolver<any, any>;
   errors: Array<GraphQLError>;
+  errorPropagation: boolean;
 }
 
 /**
@@ -152,6 +155,13 @@ export interface ExecutionArgs {
   fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
   typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
   subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
+  /**
+   * Set to `false` to disable error propagation. Experimental.
+   * TODO: describe what this does
+   *
+   * @experimental
+   */
+    errorPropagation?: boolean;
 }
 
 /**
@@ -286,6 +296,7 @@ export function buildExecutionContext(
     fieldResolver,
     typeResolver,
     subscribeFieldResolver,
+    errorPropagation
   } = args;
 
   let operation: OperationDefinitionNode | undefined;
@@ -347,6 +358,7 @@ export function buildExecutionContext(
     typeResolver: typeResolver ?? defaultTypeResolver,
     subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
     errors: [],
+    errorPropagation: errorPropagation ?? true,
   };
 }
 
@@ -585,6 +597,7 @@ export function buildResolveInfo(
     rootValue: exeContext.rootValue,
     operation: exeContext.operation,
     variableValues: exeContext.variableValues,
+    errorPropagation: exeContext.errorPropagation,
   };
 }
 
@@ -658,6 +671,37 @@ function completeValue(
     return completed;
   }
 
+    // If field type is SemanticNonNull, complete for inner type, and throw field error
+  // if result is null and an error doesn't exist.
+  if (isSemanticNonNullType(returnType)) {
+    const completed = completeValue(
+      exeContext,
+      returnType.ofType,
+      fieldNodes,
+      info,
+      path,
+      result,
+    );
+    if (completed === null) {
+      throw new Error(
+        `Cannot return null for semantic-non-nullable field ${info.parentType.name}.${info.fieldName}.`,
+      );
+    }
+    return completed;
+  }
+
+  // If field type is SemanticNullable, complete for inner type
+  if (isSemanticNullableType(returnType)) {
+    return completeValue(
+      exeContext,
+      returnType.ofType,
+      fieldNodes,
+      info,
+      path,
+      result,
+    );
+  }
+
   // If result value is null or undefined then return null.
   if (result == null) {
     return null;
diff --git a/src/graphql.ts b/src/graphql.ts
index bc6fb9bb72..d3f05f991e 100644
--- a/src/graphql.ts
+++ b/src/graphql.ts
@@ -66,6 +66,12 @@ export interface GraphQLArgs {
   operationName?: Maybe<string>;
   fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
   typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
+  /**
+   * Set to `false` to disable error propagation. Experimental.
+   *
+   * @experimental
+   */
+  errorPropagation?: boolean;
 }
 
 export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
@@ -106,6 +112,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
     operationName,
     fieldResolver,
     typeResolver,
+    errorPropagation,
   } = args;
 
   // Validate Schema
@@ -138,5 +145,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
     operationName,
     fieldResolver,
     typeResolver,
+    errorPropagation,
   });
 }
diff --git a/src/index.ts b/src/index.ts
index 73c713a203..a911680a67 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -48,6 +48,7 @@ export {
   GraphQLInputObjectType,
   GraphQLList,
   GraphQLNonNull,
+  GraphQLSemanticNonNull,
   // Standard GraphQL Scalars
   specifiedScalarTypes,
   GraphQLInt,
@@ -74,6 +75,7 @@ export {
   __Schema,
   __Directive,
   __DirectiveLocation,
+  __TypeNullability,
   __Type,
   __Field,
   __InputValue,
@@ -95,6 +97,7 @@ export {
   isInputObjectType,
   isListType,
   isNonNullType,
+  isSemanticNonNullType,
   isInputType,
   isOutputType,
   isLeafType,
@@ -120,6 +123,7 @@ export {
   assertInputObjectType,
   assertListType,
   assertNonNullType,
+  assertSemanticNonNullType,
   assertInputType,
   assertOutputType,
   assertLeafType,
@@ -286,6 +290,7 @@ export type {
   TypeNode,
   NamedTypeNode,
   ListTypeNode,
+  SemanticNonNullTypeNode,
   NonNullTypeNode,
   TypeSystemDefinitionNode,
   SchemaDefinitionNode,
@@ -481,6 +486,7 @@ export type {
   IntrospectionNamedTypeRef,
   IntrospectionListTypeRef,
   IntrospectionNonNullTypeRef,
+  IntrospectionSemanticNonNullTypeRef,
   IntrospectionField,
   IntrospectionInputValue,
   IntrospectionEnumValue,
diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts
index caa922a27d..9c23d6d46b 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -657,4 +657,68 @@ describe('Parser', () => {
       });
     });
   });
+
+  describe('parseDocumentDirective', () => {
+    it('doesnt throw on document-level directive', () => {
+      parse(dedent`
+        @SemanticNullability
+        type Query {
+          hello: String
+          world: String?
+          foo: String!
+        }
+      `);
+    });
+
+    it('parses semantic-non-null types', () => {
+      const result = parseType('MyType', { allowSemanticNullability: true });
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SEMANTIC_NON_NULL_TYPE,
+        loc: { start: 0, end: 6 },
+        type: {
+          kind: Kind.NAMED_TYPE,
+          loc: { start: 0, end: 6 },
+          name: {
+            kind: Kind.NAME,
+            loc: { start: 0, end: 6 },
+            value: 'MyType',
+          },
+        },
+      });
+    });
+
+    it('parses nullable types', () => {
+      const result = parseType('MyType?', { allowSemanticNullability: true });
+      expectJSON(result).toDeepEqual({
+        kind: Kind.SEMANTIC_NULLABLE_TYPE,
+        loc: { start: 0, end: 7 },
+        type: {
+          kind: Kind.NAMED_TYPE,
+          loc: { start: 0, end: 6 },
+          name: {
+            kind: Kind.NAME,
+            loc: { start: 0, end: 6 },
+            value: 'MyType',
+          },
+        },
+      });
+    });
+
+    it('parses non-nullable types', () => {
+      const result = parseType('MyType!', { allowSemanticNullability: true });
+      expectJSON(result).toDeepEqual({
+        kind: Kind.NON_NULL_TYPE,
+        loc: { start: 0, end: 7 },
+        type: {
+          kind: Kind.NAMED_TYPE,
+          loc: { start: 0, end: 6 },
+          name: {
+            kind: Kind.NAME,
+            loc: { start: 0, end: 6 },
+            value: 'MyType',
+          },
+        },
+      });
+    });
+  });
 });
diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts
index 13477f8de9..32ef7d1fe1 100644
--- a/src/language/__tests__/predicates-test.ts
+++ b/src/language/__tests__/predicates-test.ts
@@ -92,6 +92,7 @@ describe('AST node predicates', () => {
       'NamedType',
       'ListType',
       'NonNullType',
+      'SemanticNonNullType',
     ]);
   });
 
diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts
index 41cf6c5419..a5f803bc1d 100644
--- a/src/language/__tests__/schema-printer-test.ts
+++ b/src/language/__tests__/schema-printer-test.ts
@@ -5,7 +5,7 @@ import { dedent } from '../../__testUtils__/dedent';
 import { kitchenSinkSDL } from '../../__testUtils__/kitchenSinkSDL';
 
 import { Kind } from '../kinds';
-import { parse } from '../parser';
+import { parse, parseType } from '../parser';
 import { print } from '../printer';
 
 describe('Printer: SDL document', () => {
@@ -180,4 +180,31 @@ describe('Printer: SDL document', () => {
       }
     `);
   });
+
+  it('prints NamedType', () => {
+    expect(
+      print(parseType('MyType', { allowSemanticNullability: false })),
+    ).to.equal(dedent`MyType`);
+  });
+
+  it('prints SemanticNullableType', () => {
+    expect(
+      print(parseType('MyType?', { allowSemanticNullability: true })),
+    ).to.equal(dedent`MyType?`);
+  });
+
+  it('prints SemanticNonNullType', () => {
+    expect(
+      print(parseType('MyType', { allowSemanticNullability: true })),
+    ).to.equal(dedent`MyType`);
+  });
+
+  it('prints NonNullType', () => {
+    expect(
+      print(parseType('MyType!', { allowSemanticNullability: true })),
+    ).to.equal(dedent`MyType!`);
+    expect(
+      print(parseType('MyType!', { allowSemanticNullability: false })),
+    ).to.equal(dedent`MyType!`);
+  });
 });
diff --git a/src/language/ast.ts b/src/language/ast.ts
index 6137eb6c1a..dbe03aad06 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -161,6 +161,8 @@ export type ASTNode =
   | NamedTypeNode
   | ListTypeNode
   | NonNullTypeNode
+  | SemanticNonNullTypeNode
+  | SemanticNullableTypeNode
   | SchemaDefinitionNode
   | OperationTypeDefinitionNode
   | ScalarTypeDefinitionNode
@@ -235,6 +237,8 @@ export const QueryDocumentKeys: {
   NamedType: ['name'],
   ListType: ['type'],
   NonNullType: ['type'],
+  SemanticNonNullType: ['type'],
+  SemanticNullableType: ['type'],
 
   SchemaDefinition: ['description', 'directives', 'operationTypes'],
   OperationTypeDefinition: ['type'],
@@ -519,9 +523,27 @@ export interface ConstDirectiveNode {
   readonly arguments?: ReadonlyArray<ConstArgumentNode>;
 }
 
+
+export interface SemanticNonNullTypeNode {
+  readonly kind: Kind.SEMANTIC_NON_NULL_TYPE;
+  readonly loc?: Location;
+  readonly type: NamedTypeNode | ListTypeNode;
+}
+
+export interface SemanticNullableTypeNode {
+  readonly kind: Kind.SEMANTIC_NULLABLE_TYPE;
+  readonly loc?: Location;
+  readonly type: NamedTypeNode | ListTypeNode;
+}
+
 /** Type Reference */
 
-export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode;
+export type TypeNode =
+  | NamedTypeNode
+  | ListTypeNode
+  | NonNullTypeNode
+  | SemanticNonNullTypeNode
+  | SemanticNullableTypeNode;
 
 export interface NamedTypeNode {
   readonly kind: Kind.NAMED_TYPE;
diff --git a/src/language/index.ts b/src/language/index.ts
index ec4d195e1a..a760fd21b3 100644
--- a/src/language/index.ts
+++ b/src/language/index.ts
@@ -67,6 +67,7 @@ export type {
   NamedTypeNode,
   ListTypeNode,
   NonNullTypeNode,
+  SemanticNonNullTypeNode,
   TypeSystemDefinitionNode,
   SchemaDefinitionNode,
   OperationTypeDefinitionNode,
diff --git a/src/language/kinds.ts b/src/language/kinds.ts
index cd05f66a3b..7111a94834 100644
--- a/src/language/kinds.ts
+++ b/src/language/kinds.ts
@@ -37,6 +37,8 @@ enum Kind {
   NAMED_TYPE = 'NamedType',
   LIST_TYPE = 'ListType',
   NON_NULL_TYPE = 'NonNullType',
+  SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType',
+  SEMANTIC_NULLABLE_TYPE = 'SemanticNullableType',
 
   /** Type System Definitions */
   SCHEMA_DEFINITION = 'SchemaDefinition',
diff --git a/src/language/lexer.ts b/src/language/lexer.ts
index 818f81b286..b41ae415ac 100644
--- a/src/language/lexer.ts
+++ b/src/language/lexer.ts
@@ -91,6 +91,7 @@ export class Lexer {
 export function isPunctuatorTokenKind(kind: TokenKind): boolean {
   return (
     kind === TokenKind.BANG ||
+    kind === TokenKind.QUESTION_MARK ||
     kind === TokenKind.DOLLAR ||
     kind === TokenKind.AMP ||
     kind === TokenKind.PAREN_L ||
@@ -246,9 +247,11 @@ function readNextToken(lexer: Lexer, start: number): Token {
       //   - FloatValue
       //   - StringValue
       //
-      // Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | }
+      // Punctuator :: one of ! ? $ & ( ) ... : = @ [ ] { | }
       case 0x0021: // !
         return createToken(lexer, TokenKind.BANG, position, position + 1);
+      case 0x003f: // ?
+        return createToken(lexer, TokenKind.QUESTION_MARK, position, position + 1);
       case 0x0024: // $
         return createToken(lexer, TokenKind.DOLLAR, position, position + 1);
       case 0x0026: // &
diff --git a/src/language/parser.ts b/src/language/parser.ts
index 03e4166210..84131bd603 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -51,6 +51,8 @@ import type {
   SelectionNode,
   SelectionSetNode,
   StringValueNode,
+  SemanticNonNullTypeNode,
+  SemanticNullableTypeNode,
   Token,
   TypeNode,
   TypeSystemExtensionNode,
@@ -103,6 +105,18 @@ export interface ParseOptions {
    * ```
    */
   allowLegacyFragmentVariables?: boolean;
+
+  /**
+   * When enabled, the parser will understand and parse semantic nullability
+   * annotations. This means that every type suffixed with `!` will remain
+   * non-nulllable, every type suffxed with `?` will be the classic nullable, and
+   * types without a suffix will be semantically nullable. Semantic nullability
+   * will be the new default when this is enabled. A semantically nullable type
+   * can only be null when there's an error assocaited with the field.
+   *
+   * @experimental
+   */
+  allowSemanticNullability?: boolean;
 }
 
 /**
@@ -258,6 +272,16 @@ export class Parser {
    *   - InputObjectTypeDefinition
    */
   parseDefinition(): DefinitionNode {
+    const directives = this.parseDirectives(false);
+    // If a document-level SemanticNullability directive exists as
+    // the first element in a document, then all parsing will
+    // happen in SemanticNullability mode.
+    for (const directive of directives) {
+      if (directive.name.value === 'SemanticNullability') {
+        this._options.allowSemanticNullability = true;
+      }
+    }
+
     if (this.peek(TokenKind.BRACE_L)) {
       return this.parseOperationDefinition();
     }
diff --git a/src/language/predicates.ts b/src/language/predicates.ts
index a390f4ee55..d528e6c3c2 100644
--- a/src/language/predicates.ts
+++ b/src/language/predicates.ts
@@ -67,7 +67,9 @@ export function isTypeNode(node: ASTNode): node is TypeNode {
   return (
     node.kind === Kind.NAMED_TYPE ||
     node.kind === Kind.LIST_TYPE ||
-    node.kind === Kind.NON_NULL_TYPE
+    node.kind === Kind.NON_NULL_TYPE ||
+    node.kind === Kind.SEMANTIC_NON_NULL_TYPE ||
+    node.kind === Kind.SEMANTIC_NULLABLE_TYPE
   );
 }
 
diff --git a/src/language/printer.ts b/src/language/printer.ts
index e95c118d8b..17b805e624 100644
--- a/src/language/printer.ts
+++ b/src/language/printer.ts
@@ -6,6 +6,13 @@ import { printString } from './printString';
 import type { ASTReducer } from './visitor';
 import { visit } from './visitor';
 
+/**
+ * Configuration options to control parser behavior
+ */
+export interface PrintOptions {
+  useSemanticNullability?: boolean;
+}
+
 /**
  * Converts an AST into a string, using one set of reasonable
  * formatting rules.
@@ -131,6 +138,8 @@ const printDocASTReducer: ASTReducer<string> = {
   NamedType: { leave: ({ name }) => name },
   ListType: { leave: ({ type }) => '[' + type + ']' },
   NonNullType: { leave: ({ type }) => type + '!' },
+  SemanticNonNullType: { leave: ({ type }) => type },
+  SemanticNullableType: { leave: ({ type }) => type + '?' },
 
   // Type System Definitions
 
diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts
index 0c260df99e..0b651d36b0 100644
--- a/src/language/tokenKind.ts
+++ b/src/language/tokenKind.ts
@@ -6,6 +6,7 @@ enum TokenKind {
   SOF = '<SOF>',
   EOF = '<EOF>',
   BANG = '!',
+  QUESTION_MARK = '?',
   DOLLAR = '$',
   AMP = '&',
   PAREN_L = '(',
diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts
index 8c5cacba0d..08273f495f 100644
--- a/src/type/__tests__/introspection-test.ts
+++ b/src/type/__tests__/introspection-test.ts
@@ -32,7 +32,7 @@ describe('Introspection', () => {
     expect(result).to.deep.equal({
       data: {
         __schema: {
-          queryType: { name: 'SomeObject', kind: 'OBJECT' },
+          queryType: { name: 'SomeObject' },
           mutationType: null,
           subscriptionType: null,
           types: [
@@ -437,6 +437,11 @@ describe('Introspection', () => {
                   isDeprecated: false,
                   deprecationReason: null,
                 },
+                {
+                  name: 'SEMANTIC_NON_NULL',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
               ],
               possibleTypes: null,
             },
@@ -506,7 +511,21 @@ describe('Introspection', () => {
                 },
                 {
                   name: 'type',
-                  args: [],
+                  args: [
+                    {
+                      name: 'nullability',
+                      type: {
+                        kind: 'NON_NULL',
+                        name: null,
+                        ofType: {
+                          kind: 'ENUM',
+                          name: '__TypeNullability',
+                          ofType: null,
+                        },
+                      },
+                      defaultValue: 'AUTO',
+                    },
+                  ],
                   type: {
                     kind: 'NON_NULL',
                     name: null,
@@ -640,6 +659,37 @@ describe('Introspection', () => {
               enumValues: null,
               possibleTypes: null,
             },
+            {
+              kind: 'ENUM',
+              name: '__TypeNullability',
+              specifiedByURL: null,
+              fields: null,
+              inputFields: null,
+              interfaces: null,
+              enumValues: [
+                {
+                  name: 'AUTO',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
+                {
+                  name: 'TRADITIONAL',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
+                {
+                  name: 'SEMANTIC',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
+                {
+                  name: 'FULL',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
+              ],
+              possibleTypes: null,
+            },
             {
               kind: 'OBJECT',
               name: '__EnumValue',
diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts
index 81e721e7df..774199a368 100644
--- a/src/type/__tests__/predicate-test.ts
+++ b/src/type/__tests__/predicate-test.ts
@@ -23,6 +23,7 @@ import {
   assertObjectType,
   assertOutputType,
   assertScalarType,
+  assertSemanticNonNullType,
   assertType,
   assertUnionType,
   assertWrappingType,
@@ -35,6 +36,7 @@ import {
   GraphQLNonNull,
   GraphQLObjectType,
   GraphQLScalarType,
+  GraphQLSemanticNonNull,
   GraphQLUnionType,
   isAbstractType,
   isCompositeType,
@@ -52,6 +54,7 @@ import {
   isRequiredArgument,
   isRequiredInputField,
   isScalarType,
+  isSemanticNonNullType,
   isType,
   isUnionType,
   isWrappingType,
@@ -298,6 +301,47 @@ describe('Type predicates', () => {
       expect(() =>
         assertNonNullType(new GraphQLList(new GraphQLNonNull(ObjectType))),
       ).to.throw();
+      expect(isNonNullType(new GraphQLSemanticNonNull(ObjectType))).to.equal(
+        false,
+      );
+      expect(() =>
+        assertNonNullType(new GraphQLSemanticNonNull(ObjectType)),
+      ).to.throw();
+    });
+  });
+
+  describe('isSemanticNonNullType', () => {
+    it('returns true for a semantic-non-null wrapped type', () => {
+      expect(
+        isSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)),
+      ).to.equal(true);
+      expect(() =>
+        assertSemanticNonNullType(new GraphQLSemanticNonNull(ObjectType)),
+      ).to.not.throw();
+    });
+
+    it('returns false for an unwrapped type', () => {
+      expect(isSemanticNonNullType(ObjectType)).to.equal(false);
+      expect(() => assertSemanticNonNullType(ObjectType)).to.throw();
+    });
+
+    it('returns false for a not non-null wrapped type', () => {
+      expect(
+        isSemanticNonNullType(
+          new GraphQLList(new GraphQLSemanticNonNull(ObjectType)),
+        ),
+      ).to.equal(false);
+      expect(() =>
+        assertSemanticNonNullType(
+          new GraphQLList(new GraphQLSemanticNonNull(ObjectType)),
+        ),
+      ).to.throw();
+      expect(isSemanticNonNullType(new GraphQLNonNull(ObjectType))).to.equal(
+        false,
+      );
+      expect(() =>
+        assertSemanticNonNullType(new GraphQLNonNull(ObjectType)),
+      ).to.throw();
     });
   });
 
@@ -476,6 +520,12 @@ describe('Type predicates', () => {
       expect(() =>
         assertWrappingType(new GraphQLNonNull(ObjectType)),
       ).to.not.throw();
+      expect(isWrappingType(new GraphQLSemanticNonNull(ObjectType))).to.equal(
+        true,
+      );
+      expect(() =>
+        assertWrappingType(new GraphQLSemanticNonNull(ObjectType)),
+      ).to.not.throw();
     });
 
     it('returns false for unwrapped types', () => {
@@ -497,6 +547,14 @@ describe('Type predicates', () => {
       expect(() =>
         assertNullableType(new GraphQLList(new GraphQLNonNull(ObjectType))),
       ).to.not.throw();
+      expect(
+        isNullableType(new GraphQLList(new GraphQLSemanticNonNull(ObjectType))),
+      ).to.equal(true);
+      expect(() =>
+        assertNullableType(
+          new GraphQLList(new GraphQLSemanticNonNull(ObjectType)),
+        ),
+      ).to.not.throw();
     });
 
     it('returns false for non-null types', () => {
@@ -504,6 +562,12 @@ describe('Type predicates', () => {
       expect(() =>
         assertNullableType(new GraphQLNonNull(ObjectType)),
       ).to.throw();
+      expect(isNullableType(new GraphQLSemanticNonNull(ObjectType))).to.equal(
+        false,
+      );
+      expect(() =>
+        assertNullableType(new GraphQLSemanticNonNull(ObjectType)),
+      ).to.throw();
     });
   });
 
@@ -701,4 +765,4 @@ describe('Directive predicates', () => {
       expect(isSpecifiedDirective(Directive)).to.equal(false);
     });
   });
-});
+});
\ No newline at end of file
diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts
index 8a31b50ada..dc2c7c75c8 100644
--- a/src/type/__tests__/schema-test.ts
+++ b/src/type/__tests__/schema-test.ts
@@ -301,6 +301,7 @@ describe('Type System: Schema', () => {
       '__TypeKind',
       '__Field',
       '__InputValue',
+      '__TypeNullability',
       '__EnumValue',
       '__Directive',
       '__DirectiveLocation',
diff --git a/src/type/definition.ts b/src/type/definition.ts
index 7eaac560dc..ff1238793b 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -66,7 +66,25 @@ export type GraphQLType =
       | GraphQLEnumType
       | GraphQLInputObjectType
       | GraphQLList<GraphQLType>
-    >;
+    >
+  |  GraphQLSemanticNonNull<
+    | GraphQLScalarType
+    | GraphQLObjectType
+    | GraphQLInterfaceType
+    | GraphQLUnionType
+    | GraphQLEnumType
+    | GraphQLInputObjectType
+    | GraphQLList<GraphQLType>
+  >
+  | GraphQLSemanticNullable<
+    | GraphQLScalarType
+    | GraphQLObjectType
+    | GraphQLInterfaceType
+    | GraphQLUnionType
+    | GraphQLEnumType
+    | GraphQLInputObjectType
+    | GraphQLList<GraphQLType>
+  >;
 
 export function isType(type: unknown): type is GraphQLType {
   return (
@@ -77,7 +95,9 @@ export function isType(type: unknown): type is GraphQLType {
     isEnumType(type) ||
     isInputObjectType(type) ||
     isListType(type) ||
-    isNonNullType(type)
+    isNonNullType(type) ||
+    isNonNullType(type) ||
+    isSemanticNonNullType(type)
   );
 }
 
@@ -203,6 +223,58 @@ export function assertNonNullType(type: unknown): GraphQLNonNull<GraphQLType> {
   return type;
 }
 
+export function isSemanticNonNullType(
+  type: GraphQLInputType,
+): type is GraphQLSemanticNonNull<GraphQLInputType>;
+export function isSemanticNonNullType(
+  type: GraphQLOutputType,
+): type is GraphQLSemanticNonNull<GraphQLOutputType>;
+export function isSemanticNonNullType(
+  type: unknown,
+): type is GraphQLSemanticNonNull<GraphQLType>;
+export function isSemanticNonNullType(
+  type: unknown,
+): type is GraphQLSemanticNonNull<GraphQLType> {
+  return instanceOf(type, GraphQLSemanticNonNull);
+}
+
+export function assertSemanticNonNullType(
+  type: unknown,
+): GraphQLSemanticNonNull<GraphQLType> {
+  if (!isSemanticNonNullType(type)) {
+    throw new Error(
+      `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`,
+    );
+  }
+  return type;
+}
+
+export function isSemanticNullableType(
+  type: GraphQLInputType,
+): type is GraphQLSemanticNullable<GraphQLInputType>;
+export function isSemanticNullableType(
+  type: GraphQLOutputType,
+): type is GraphQLSemanticNullable<GraphQLOutputType>;
+export function isSemanticNullableType(
+  type: unknown,
+): type is GraphQLSemanticNullable<GraphQLType>;
+export function isSemanticNullableType(
+  type: unknown,
+): type is GraphQLSemanticNullable<GraphQLType> {
+  return instanceOf(type, GraphQLSemanticNullable);
+}
+
+export function assertSemanticNullableType(
+  type: unknown,
+): GraphQLSemanticNullable<GraphQLType> {
+  if (!isSemanticNullableType(type)) {
+    throw new Error(
+      `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`,
+    );
+  }
+  return type;
+}
+
 /**
  * These types may be used as input types for arguments and directives.
  */
@@ -223,7 +295,9 @@ export function isInputType(type: unknown): type is GraphQLInputType {
     isScalarType(type) ||
     isEnumType(type) ||
     isInputObjectType(type) ||
-    (isWrappingType(type) && isInputType(type.ofType))
+    (!isSemanticNonNullType(type) &&
+      isWrappingType(type) &&
+      isInputType(type.ofType))
   );
 }
 
@@ -251,7 +325,13 @@ export type GraphQLOutputType =
       | GraphQLUnionType
       | GraphQLEnumType
       | GraphQLList<GraphQLOutputType>
-    >;
+    >   | GraphQLSemanticNonNull<
+    | GraphQLScalarType
+    | GraphQLObjectType
+    | GraphQLInterfaceType
+    | GraphQLUnionType
+    | GraphQLEnumType
+    | GraphQLList<GraphQLOutputType> >;
 
 export function isOutputType(type: unknown): type is GraphQLOutputType {
   return (
@@ -414,16 +494,118 @@ export class GraphQLNonNull<T extends GraphQLNullableType> {
   }
 }
 
+/**
+ * Semantic-Non-Null Type Wrapper
+ *
+ * A semantic-non-null is a wrapping type which points to another type.
+ * Semantic-non-null types enforce that their values are never null unless
+ * caused by an error being raised. It is useful for fields which you can make
+ * a guarantee on non-nullability in a no-error case, for example when you know
+ * that a related entity must exist (but acknowledge that retrieving it may
+ * produce an error).
+ *
+ * Example:
+ *
+ * ```ts
+ * const RowType = new GraphQLObjectType({
+ *   name: 'Row',
+ *   fields: () => ({
+ *     email: { type: new GraphQLSemanticNonNull(GraphQLString) },
+ *   })
+ * })
+ * ```
+ * Note: the enforcement of non-nullability occurs within the executor.
+ *
+ * @experimental
+ */
+export class GraphQLSemanticNonNull<T extends GraphQLNullableType> {
+  readonly ofType: T;
+
+  constructor(ofType: T) {
+    devAssert(
+      isNullableType(ofType),
+      `Expected ${inspect(ofType)} to be a GraphQL nullable type.`,
+    );
+
+    this.ofType = ofType;
+  }
+
+  get [Symbol.toStringTag]() {
+    return 'GraphQLSemanticNonNull';
+  }
+
+  toString(): string {
+    return String(this.ofType);
+  }
+
+  toJSON(): string {
+    return this.toString();
+  }
+}
+
+/**
+ * Semantic-Nullable Type Wrapper
+ *
+ * A semantic-nullable is a wrapping type which points to another type.
+ * Semantic-nullable types allow their values to be null.
+ *
+ * Example:
+ *
+ * ```ts
+ * const RowType = new GraphQLObjectType({
+ *   name: 'Row',
+ *   fields: () => ({
+ *     email: { type: new GraphQLSemanticNullable(GraphQLString) },
+ *   })
+ * })
+ * ```
+ * Note: This is equivalent to the unadorned named type that is
+ * used by GraphQL when it is not operating in SemanticNullability mode.
+ *
+ * @experimental
+ */
+export class GraphQLSemanticNullable<T extends GraphQLNullableType> {
+  readonly ofType: T;
+
+  constructor(ofType: T) {
+    devAssert(
+      isNullableType(ofType),
+      `Expected ${inspect(ofType)} to be a GraphQL nullable type.`,
+    );
+
+    this.ofType = ofType;
+  }
+
+  get [Symbol.toStringTag]() {
+    return 'GraphQLSemanticNullable';
+  }
+
+  toString(): string {
+    return String(this.ofType) + '?';
+  }
+
+  toJSON(): string {
+    return this.toString();
+  }
+}
+
 /**
  * These types wrap and modify other types
  */
 
 export type GraphQLWrappingType =
   | GraphQLList<GraphQLType>
-  | GraphQLNonNull<GraphQLType>;
+  | GraphQLNonNull<GraphQLType>
+  | GraphQLSemanticNonNull<GraphQLType>
+  | GraphQLSemanticNullable<GraphQLType>;
 
 export function isWrappingType(type: unknown): type is GraphQLWrappingType {
-  return isListType(type) || isNonNullType(type);
+  return (
+    isListType(type) ||
+    isNonNullType(type) ||
+    isSemanticNonNullType(type) ||
+    isSemanticNullableType(type)
+  );
 }
 
 export function assertWrappingType(type: unknown): GraphQLWrappingType {
@@ -446,7 +628,7 @@ export type GraphQLNullableType =
   | GraphQLList<GraphQLType>;
 
 export function isNullableType(type: unknown): type is GraphQLNullableType {
-  return isType(type) && !isNonNullType(type);
+  return isType(type) && !isNonNullType(type) && !isSemanticNonNullType(type);
 }
 
 export function assertNullableType(type: unknown): GraphQLNullableType {
@@ -458,7 +640,7 @@ export function assertNullableType(type: unknown): GraphQLNullableType {
 
 export function getNullableType(type: undefined | null): void;
 export function getNullableType<T extends GraphQLNullableType>(
-  type: T | GraphQLNonNull<T>,
+  type: T | GraphQLNonNull<T> | GraphQLSemanticNonNull<T>,
 ): T;
 export function getNullableType(
   type: Maybe<GraphQLType>,
@@ -467,12 +649,14 @@ export function getNullableType(
   type: Maybe<GraphQLType>,
 ): GraphQLNullableType | undefined {
   if (type) {
-    return isNonNullType(type) ? type.ofType : type;
+    return isNonNullType(type) || isSemanticNonNullType(type)
+      ? type.ofType
+      : type;
   }
 }
 
 /**
- * These named types do not include modifiers like List or NonNull.
+ * These named types do not include modifiers like List, NonNull, or SemanticNonNull
  */
 export type GraphQLNamedType = GraphQLNamedInputType | GraphQLNamedOutputType;
 
@@ -988,6 +1172,8 @@ export interface GraphQLResolveInfo {
   readonly rootValue: unknown;
   readonly operation: OperationDefinitionNode;
   readonly variableValues: { [variable: string]: unknown };
+  /** @experimental */
+  readonly errorPropagation: boolean;
 }
 
 /**
diff --git a/src/type/index.ts b/src/type/index.ts
index cf276d1e02..e6cf627bd5 100644
--- a/src/type/index.ts
+++ b/src/type/index.ts
@@ -23,6 +23,7 @@ export {
   isInputObjectType,
   isListType,
   isNonNullType,
+  isSemanticNonNullType,
   isInputType,
   isOutputType,
   isLeafType,
@@ -43,6 +44,7 @@ export {
   assertInputObjectType,
   assertListType,
   assertNonNullType,
+  assertSemanticNonNullType,
   assertInputType,
   assertOutputType,
   assertLeafType,
@@ -64,6 +66,7 @@ export {
   // Type Wrappers
   GraphQLList,
   GraphQLNonNull,
+  GraphQLSemanticNonNull,
 } from './definition';
 
 export type {
@@ -167,6 +170,7 @@ export {
   __Schema,
   __Directive,
   __DirectiveLocation,
+  __TypeNullability,
   __Type,
   __Field,
   __InputValue,
diff --git a/src/type/introspection.ts b/src/type/introspection.ts
index 2c66ca5098..7dbce27eec 100644
--- a/src/type/introspection.ts
+++ b/src/type/introspection.ts
@@ -19,6 +19,7 @@ import {
   GraphQLList,
   GraphQLNonNull,
   GraphQLObjectType,
+  GraphQLSemanticNonNull,
   isAbstractType,
   isEnumType,
   isInputObjectType,
@@ -27,6 +28,7 @@ import {
   isNonNullType,
   isObjectType,
   isScalarType,
+  isSemanticNonNullType,
   isUnionType,
 } from './definition';
 import type { GraphQLDirective } from './directives';
@@ -204,6 +206,40 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({
   },
 });
 
+// TODO: rename enum and options
+enum TypeNullability {
+  AUTO = 'AUTO',
+  TRADITIONAL = 'TRADITIONAL',
+  SEMANTIC = 'SEMANTIC',
+  FULL = 'FULL',
+}
+
+// TODO: rename
+export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({
+  name: '__TypeNullability',
+  description: 'TODO',
+  values: {
+    AUTO: {
+      value: TypeNullability.AUTO,
+      description:
+        'Determines nullability mode based on errorPropagation mode.',
+    },
+    TRADITIONAL: {
+      value: TypeNullability.TRADITIONAL,
+      description: 'Turn semantic-non-null types into nullable types.',
+    },
+    SEMANTIC: {
+      value: TypeNullability.SEMANTIC,
+      description: 'Turn non-null types into semantic-non-null types.',
+    },
+    FULL: {
+      value: TypeNullability.FULL,
+      description:
+        'Render the true nullability in the schema; be prepared for new types of nullability in future!',
+    },
+  },
+});
+
 export const __Type: GraphQLObjectType = new GraphQLObjectType({
   name: '__Type',
   description:
@@ -237,6 +273,9 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({
           if (isNonNullType(type)) {
             return TypeKind.NON_NULL;
           }
+          if (isSemanticNonNullType(type)) {
+            return TypeKind.SEMANTIC_NON_NULL;
+          }
           /* c8 ignore next 3 */
           // Not reachable, all possible types have been considered)
           invariant(false, `Unexpected type: "${inspect(type)}".`);
@@ -366,7 +405,25 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
       },
       type: {
         type: new GraphQLNonNull(__Type),
-        resolve: (field) => field.type,
+        args: {
+          nullability: {
+            type: new GraphQLNonNull(__TypeNullability),
+            defaultValue: TypeNullability.AUTO,
+          },
+        },
+        resolve: (field, { nullability }, _context, info) => {
+          if (nullability === TypeNullability.FULL) {
+            return field.type;
+          }
+
+          const mode =
+          nullability === TypeNullability.AUTO
+            ? info.errorPropagation
+              ? TypeNullability.TRADITIONAL
+              : TypeNullability.SEMANTIC
+              : nullability;
+          return convertOutputTypeToNullabilityMode(field.type, mode);
+        },
       },
       isDeprecated: {
         type: new GraphQLNonNull(GraphQLBoolean),
@@ -379,6 +436,37 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
     } as GraphQLFieldConfigMap<GraphQLField<unknown, unknown>, unknown>),
 });
 
+// TODO: move this elsewhere, rename, memoize
+function convertOutputTypeToNullabilityMode(
+  type: GraphQLType,
+  mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC,
+): GraphQLType {
+  if (mode === TypeNullability.TRADITIONAL) {
+    if (isNonNullType(type)) {
+      return new GraphQLNonNull(
+        convertOutputTypeToNullabilityMode(type.ofType, mode),
+      );
+    } else if (isSemanticNonNullType(type)) {
+      return convertOutputTypeToNullabilityMode(type.ofType, mode);
+    } else if (isListType(type)) {
+      return new GraphQLList(
+        convertOutputTypeToNullabilityMode(type.ofType, mode),
+      );
+    }
+    return type;
+  }
+  if (isNonNullType(type) || isSemanticNonNullType(type)) {
+    return new GraphQLSemanticNonNull(
+      convertOutputTypeToNullabilityMode(type.ofType, mode),
+    );
+  } else if (isListType(type)) {
+    return new GraphQLList(
+      convertOutputTypeToNullabilityMode(type.ofType, mode),
+    );
+  }
+  return type;
+}
+
 export const __InputValue: GraphQLObjectType = new GraphQLObjectType({
   name: '__InputValue',
   description:
@@ -452,6 +540,7 @@ enum TypeKind {
   INPUT_OBJECT = 'INPUT_OBJECT',
   LIST = 'LIST',
   NON_NULL = 'NON_NULL',
+  SEMANTIC_NON_NULL = 'SEMANTIC_NON_NULL',
 }
 export { TypeKind };
 
@@ -497,6 +586,11 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({
       description:
         'Indicates this type is a non-null. `ofType` is a valid field.',
     },
+    SEMANTIC_NON_NULL: {
+      value: TypeKind.SEMANTIC_NON_NULL,
+      description:
+        'Indicates this type is a semantic-non-null. `ofType` is a valid field.',
+    },
   },
 });
 
@@ -553,6 +647,7 @@ export const introspectionTypes: ReadonlyArray<GraphQLNamedType> =
     __Schema,
     __Directive,
     __DirectiveLocation,
+    __TypeNullability,
     __Type,
     __Field,
     __InputValue,
diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts
index 37af4a60f7..b651bf16a8 100644
--- a/src/utilities/__tests__/printSchema-test.ts
+++ b/src/utilities/__tests__/printSchema-test.ts
@@ -770,6 +770,9 @@ describe('Type System Printer', () => {
 
         """Indicates this type is a non-null. \`ofType\` is a valid field."""
         NON_NULL
+
+        """Indicates this type is a semantic-non-null. \`ofType\` is a valid field."""
+        SEMANTIC_NON_NULL
       }
 
       """
@@ -779,7 +782,7 @@ describe('Type System Printer', () => {
         name: String!
         description: String
         args(includeDeprecated: Boolean = false): [__InputValue!]!
-        type: __Type!
+        type(nullability: __TypeNullability! = AUTO): __Type!
         isDeprecated: Boolean!
         deprecationReason: String
       }
@@ -800,6 +803,23 @@ describe('Type System Printer', () => {
         deprecationReason: String
       }
 
+      """TODO"""
+      enum __TypeNullability {
+        """Determines nullability mode based on errorPropagation mode."""
+        AUTO
+      
+        """Turn semantic-non-null types into nullable types."""
+        TRADITIONAL
+      
+        """Turn non-null types into semantic-non-null types."""
+        SEMANTIC
+      
+        """
+        Render the true nullability in the schema; be prepared for new types of nullability in future!
+        """
+        FULL
+      }
+
       """
       One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.
       """
diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts
index 83f6abada8..bcc69557cb 100644
--- a/src/utilities/buildClientSchema.ts
+++ b/src/utilities/buildClientSchema.ts
@@ -23,6 +23,7 @@ import {
   GraphQLObjectType,
   GraphQLScalarType,
   GraphQLUnionType,
+  GraphQLSemanticNonNull,
   isInputType,
   isOutputType,
 } from '../type/definition';
@@ -137,6 +138,14 @@ export function buildClientSchema(
       const nullableType = getType(nullableRef);
       return new GraphQLNonNull(assertNullableType(nullableType));
     }
+    if (typeRef.kind === TypeKind.SEMANTIC_NON_NULL) {
+      const nullableRef = typeRef.ofType;
+      if (!nullableRef) {
+        throw new Error('Decorated type deeper than introspection query.');
+      }
+      const nullableType = getType(nullableRef);
+      return new GraphQLSemanticNonNull(assertNullableType(nullableType));
+    }
     return getNamedType(typeRef);
   }
 
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index d53752d919..452bc8cdab 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -54,6 +54,8 @@ import {
   GraphQLObjectType,
   GraphQLScalarType,
   GraphQLUnionType,
+  GraphQLSemanticNonNull,
+  GraphQLSemanticNullable,
   isEnumType,
   isInputObjectType,
   isInterfaceType,
@@ -61,6 +63,7 @@ import {
   isNonNullType,
   isObjectType,
   isScalarType,
+  isSemanticNonNullType,
   isUnionType,
 } from '../type/definition';
 import {
@@ -225,6 +228,10 @@ export function extendSchemaImpl(
       // @ts-expect-error
       return new GraphQLNonNull(replaceType(type.ofType));
     }
+    if (isSemanticNonNullType(type)) {
+      // @ts-expect-error
+      return new GraphQLSemanticNonNull(replaceType(type.ofType));
+    }
     // @ts-expect-error FIXME
     return replaceNamedType(type);
   }
@@ -432,6 +439,12 @@ export function extendSchemaImpl(
     if (node.kind === Kind.NON_NULL_TYPE) {
       return new GraphQLNonNull(getWrappedType(node.type));
     }
+    if (node.kind === Kind.SEMANTIC_NON_NULL_TYPE) {
+      return new GraphQLSemanticNonNull(getWrappedType(node.type));
+    }
+    if (node.kind === Kind.SEMANTIC_NULLABLE_TYPE) {
+      return new GraphQLSemanticNullable(getWrappedType(node.type));
+    }
     return getNamedType(node);
   }
 
diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts
index 2489af9d62..5ed0313ae3 100644
--- a/src/utilities/findBreakingChanges.ts
+++ b/src/utilities/findBreakingChanges.ts
@@ -26,6 +26,7 @@ import {
   isRequiredArgument,
   isRequiredInputField,
   isScalarType,
+  isSemanticNonNullType,
   isUnionType,
 } from '../type/definition';
 import { isSpecifiedScalarType } from '../type/scalars';
@@ -458,6 +459,9 @@ function isChangeSafeForObjectOrInterfaceField(
         )) ||
       // moving from nullable to non-null of the same underlying type is safe
       (isNonNullType(newType) &&
+        isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ||
+      // moving from nullable to semantic-non-null of the same underlying type is safe
+      (isSemanticNonNullType(newType) &&
         isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
     );
   }
@@ -470,11 +474,28 @@ function isChangeSafeForObjectOrInterfaceField(
     );
   }
 
+  if (isSemanticNonNullType(oldType)) {
+    return (
+      // if they're both semantic-non-null, make sure the underlying types are compatible
+      (isSemanticNonNullType(newType) &&
+        isChangeSafeForObjectOrInterfaceField(
+          oldType.ofType,
+          newType.ofType,
+        )) ||
+      // moving from semantic-non-null to non-null of the same underlying type is safe
+      (isNonNullType(newType) &&
+        isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType))
+    );
+  }
+
   return (
     // if they're both named types, see if their names are equivalent
     (isNamedType(newType) && oldType.name === newType.name) ||
     // moving from nullable to non-null of the same underlying type is safe
     (isNonNullType(newType) &&
+      isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)) ||
+    // moving from nullable to semantic-non-null of the same underlying type is safe
+    (isSemanticNonNullType(newType) &&
       isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
   );
 }
diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts
index 373b474ed5..dda0e7f19a 100644
--- a/src/utilities/getIntrospectionQuery.ts
+++ b/src/utilities/getIntrospectionQuery.ts
@@ -38,6 +38,17 @@ export interface IntrospectionOptions {
    * Default: false
    */
   oneOf?: boolean;
+
+  /**
+   * Choose the type of nullability you would like to see.
+   *
+   * - AUTO: SEMANTIC if errorPropagation is set to false, otherwise TRADITIONAL
+   * - TRADITIONAL: all GraphQLSemanticNonNull will be unwrapped
+   * - SEMANTIC: all GraphQLNonNull will be converted to GraphQLSemanticNonNull
+   * - FULL: the true nullability will be returned
+   *
+   */
+  nullability?: 'AUTO' | 'TRADITIONAL' | 'SEMANTIC' | 'FULL';
 }
 
 /**
@@ -52,6 +63,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
     schemaDescription: false,
     inputValueDeprecation: false,
     oneOf: false,
+    nullability: null,
     ...options,
   };
 
@@ -70,6 +82,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
     return optionsWithDefault.inputValueDeprecation ? str : '';
   }
   const oneOf = optionsWithDefault.oneOf ? 'isOneOf' : '';
+  const nullability = optionsWithDefault.nullability;
 
   return `
     query IntrospectionQuery {
@@ -105,7 +118,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
         args${inputDeprecation('(includeDeprecated: true)')} {
           ...InputValue
         }
-        type {
+        type${nullability ? `(nullability: ${nullability})` : ''} {
           ...TypeRef
         }
         isDeprecated
@@ -285,11 +298,21 @@ export interface IntrospectionNonNullTypeRef<
   readonly ofType: T;
 }
 
+export interface IntrospectionSemanticNonNullTypeRef<
+  T extends IntrospectionTypeRef = IntrospectionTypeRef,
+> {
+  readonly kind: 'SEMANTIC_NON_NULL';
+  readonly ofType: T;
+}
+
 export type IntrospectionTypeRef =
   | IntrospectionNamedTypeRef
   | IntrospectionListTypeRef
   | IntrospectionNonNullTypeRef<
       IntrospectionNamedTypeRef | IntrospectionListTypeRef
+    >
+  | IntrospectionSemanticNonNullTypeRef<
+      IntrospectionNamedTypeRef | IntrospectionListTypeRef
     >;
 
 export type IntrospectionOutputTypeRef =
diff --git a/src/utilities/index.ts b/src/utilities/index.ts
index 452b975233..fa69583012 100644
--- a/src/utilities/index.ts
+++ b/src/utilities/index.ts
@@ -20,6 +20,7 @@ export type {
   IntrospectionNamedTypeRef,
   IntrospectionListTypeRef,
   IntrospectionNonNullTypeRef,
+  IntrospectionSemanticNonNullTypeRef,
   IntrospectionField,
   IntrospectionInputValue,
   IntrospectionEnumValue,
diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts
index 26b6908c9f..5beb646859 100644
--- a/src/utilities/lexicographicSortSchema.ts
+++ b/src/utilities/lexicographicSortSchema.ts
@@ -19,6 +19,7 @@ import {
   GraphQLList,
   GraphQLNonNull,
   GraphQLObjectType,
+  GraphQLSemanticNonNull,
   GraphQLUnionType,
   isEnumType,
   isInputObjectType,
@@ -27,6 +28,7 @@ import {
   isNonNullType,
   isObjectType,
   isScalarType,
+  isSemanticNonNullType,
   isUnionType,
 } from '../type/definition';
 import { GraphQLDirective } from '../type/directives';
@@ -62,6 +64,9 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema {
     } else if (isNonNullType(type)) {
       // @ts-expect-error
       return new GraphQLNonNull(replaceType(type.ofType));
+    } else if (isSemanticNonNullType(type)) {
+      // @ts-expect-error
+      return new GraphQLSemanticNonNull(replaceType(type.ofType));
     }
     // @ts-expect-error FIXME: TS Conversion
     return replaceNamedType<GraphQLNamedType>(type);
diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts
index 287be40bfe..13311780ff 100644
--- a/src/utilities/typeComparators.ts
+++ b/src/utilities/typeComparators.ts
@@ -5,6 +5,7 @@ import {
   isListType,
   isNonNullType,
   isObjectType,
+  isSemanticNonNullType,
 } from '../type/definition';
 import type { GraphQLSchema } from '../type/schema';
 
@@ -22,6 +23,11 @@ export function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean {
     return isEqualType(typeA.ofType, typeB.ofType);
   }
 
+  // If either type is semantic-non-null, the other must also be semantic-non-null.
+  if (isSemanticNonNullType(typeA) && isSemanticNonNullType(typeB)) {
+    return isEqualType(typeA.ofType, typeB.ofType);
+  }
+
   // If either type is a list, the other must also be a list.
   if (isListType(typeA) && isListType(typeB)) {
     return isEqualType(typeA.ofType, typeB.ofType);
@@ -52,8 +58,17 @@ export function isTypeSubTypeOf(
     }
     return false;
   }
-  if (isNonNullType(maybeSubType)) {
-    // If superType is nullable, maybeSubType may be non-null or nullable.
+
+  // If superType is semantic-non-null, maybeSubType must be semantic-non-null or non-null.
+  if (isSemanticNonNullType(superType)) {
+    if (isNonNullType(maybeSubType) || isSemanticNonNullType(maybeSubType)) {
+      return isTypeSubTypeOf(schema, maybeSubType.ofType, superType.ofType);
+    }
+    return false;
+  }
+
+  if (isNonNullType(maybeSubType) || isSemanticNonNullType(maybeSubType)) {
+    // If superType is nullable, maybeSubType may be non-null, semantic-non-null, or nullable.
     return isTypeSubTypeOf(schema, maybeSubType.ofType, superType);
   }
 
diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts
index 7510df1046..c5d5f537a2 100644
--- a/src/utilities/typeFromAST.ts
+++ b/src/utilities/typeFromAST.ts
@@ -7,7 +7,11 @@ import type {
 import { Kind } from '../language/kinds';
 
 import type { GraphQLNamedType, GraphQLType } from '../type/definition';
-import { GraphQLList, GraphQLNonNull } from '../type/definition';
+import {
+  GraphQLList,
+  GraphQLNonNull,
+  GraphQLSemanticNonNull,
+} from '../type/definition';
 import type { GraphQLSchema } from '../type/schema';
 
 /**
@@ -46,6 +50,10 @@ export function typeFromAST(
       const innerType = typeFromAST(schema, typeNode.type);
       return innerType && new GraphQLNonNull(innerType);
     }
+    case Kind.SEMANTIC_NON_NULL_TYPE: {
+      const innerType = typeFromAST(schema, typeNode.type);
+      return innerType && new GraphQLSemanticNonNull(innerType);
+    }
     case Kind.NAMED_TYPE:
       return schema.getType(typeNode.name.value);
   }
diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
index 8397a35b80..182215fd3f 100644
--- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
+++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
@@ -27,6 +27,7 @@ import {
   isListType,
   isNonNullType,
   isObjectType,
+  isSemanticNonNullType,
 } from '../../type/definition';
 
 import { sortValueNode } from '../../utilities/sortValueNode';
@@ -723,6 +724,14 @@ function doTypesConflict(
   if (isNonNullType(type2)) {
     return true;
   }
+  if (isSemanticNonNullType(type1)) {
+    return isSemanticNonNullType(type2)
+      ? doTypesConflict(type1.ofType, type2.ofType)
+      : true;
+  }
+  if (isSemanticNonNullType(type2)) {
+    return true;
+  }
   if (isLeafType(type1) || isLeafType(type2)) {
     return type1 !== type2;
   }

From 0f13010005ef8441fa874bc1f9bbe9d5388c2e2b Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Sun, 2 Feb 2025 09:03:46 +0100
Subject: [PATCH 02/10] Fix a few tests

---
 src/execution/__tests__/executor-test.ts  |  2 +-
 src/execution/execute.ts                  |  6 +--
 src/language/__tests__/parser-test.ts     |  2 +-
 src/language/__tests__/predicates-test.ts |  1 +
 src/language/ast.ts                       |  1 -
 src/language/lexer.ts                     |  7 +++-
 src/language/parser.ts                    | 25 ++++++++++--
 src/type/__tests__/introspection-test.ts  |  2 +-
 src/type/__tests__/predicate-test.ts      |  2 +-
 src/type/definition.ts                    | 50 ++++++++++++-----------
 src/type/introspection.ts                 |  8 ++--
 src/utilities/buildClientSchema.ts        |  2 +-
 src/utilities/extendSchema.ts             |  2 +-
 src/utilities/typeFromAST.ts              |  5 +++
 14 files changed, 73 insertions(+), 42 deletions(-)

diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts
index afc73d5c08..a7bc1c8265 100644
--- a/src/execution/__tests__/executor-test.ts
+++ b/src/execution/__tests__/executor-test.ts
@@ -276,7 +276,7 @@ describe('Execute: Handles basic execution tasks', () => {
       schema,
       rootValue,
       operation,
-      errorPropagation: true
+      errorPropagation: true,
     });
 
     const field = operation.selectionSet.selections[0];
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 33aa1dd6f1..0bfbcf3f3e 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -161,7 +161,7 @@ export interface ExecutionArgs {
    *
    * @experimental
    */
-    errorPropagation?: boolean;
+  errorPropagation?: boolean;
 }
 
 /**
@@ -296,7 +296,7 @@ export function buildExecutionContext(
     fieldResolver,
     typeResolver,
     subscribeFieldResolver,
-    errorPropagation
+    errorPropagation,
   } = args;
 
   let operation: OperationDefinitionNode | undefined;
@@ -671,7 +671,7 @@ function completeValue(
     return completed;
   }
 
-    // If field type is SemanticNonNull, complete for inner type, and throw field error
+  // If field type is SemanticNonNull, complete for inner type, and throw field error
   // if result is null and an error doesn't exist.
   if (isSemanticNonNullType(returnType)) {
     const completed = completeValue(
diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts
index 9c23d6d46b..4c134f2be0 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -659,7 +659,7 @@ describe('Parser', () => {
   });
 
   describe('parseDocumentDirective', () => {
-    it('doesnt throw on document-level directive', () => {
+    it('doesn\'t throw on document-level directive', () => {
       parse(dedent`
         @SemanticNullability
         type Query {
diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts
index 32ef7d1fe1..aa41961177 100644
--- a/src/language/__tests__/predicates-test.ts
+++ b/src/language/__tests__/predicates-test.ts
@@ -93,6 +93,7 @@ describe('AST node predicates', () => {
       'ListType',
       'NonNullType',
       'SemanticNonNullType',
+      'SemanticNullableType',
     ]);
   });
 
diff --git a/src/language/ast.ts b/src/language/ast.ts
index dbe03aad06..57beb3c573 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -523,7 +523,6 @@ export interface ConstDirectiveNode {
   readonly arguments?: ReadonlyArray<ConstArgumentNode>;
 }
 
-
 export interface SemanticNonNullTypeNode {
   readonly kind: Kind.SEMANTIC_NON_NULL_TYPE;
   readonly loc?: Location;
diff --git a/src/language/lexer.ts b/src/language/lexer.ts
index b41ae415ac..86ff5edb6f 100644
--- a/src/language/lexer.ts
+++ b/src/language/lexer.ts
@@ -251,7 +251,12 @@ function readNextToken(lexer: Lexer, start: number): Token {
       case 0x0021: // !
         return createToken(lexer, TokenKind.BANG, position, position + 1);
       case 0x003f: // ?
-        return createToken(lexer, TokenKind.QUESTION_MARK, position, position + 1);
+        return createToken(
+          lexer,
+          TokenKind.QUESTION_MARK,
+          position,
+          position + 1,
+        );
       case 0x0024: // $
         return createToken(lexer, TokenKind.DOLLAR, position, position + 1);
       case 0x0026: // &
diff --git a/src/language/parser.ts b/src/language/parser.ts
index 84131bd603..5743eef9da 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -50,9 +50,9 @@ import type {
   SchemaExtensionNode,
   SelectionNode,
   SelectionSetNode,
-  StringValueNode,
   SemanticNonNullTypeNode,
   SemanticNullableTypeNode,
+  StringValueNode,
   Token,
   TypeNode,
   TypeSystemExtensionNode,
@@ -109,10 +109,10 @@ export interface ParseOptions {
   /**
    * When enabled, the parser will understand and parse semantic nullability
    * annotations. This means that every type suffixed with `!` will remain
-   * non-nulllable, every type suffxed with `?` will be the classic nullable, and
+   * non-nullable, every type suffixed with `?` will be the classic nullable, and
    * types without a suffix will be semantically nullable. Semantic nullability
    * will be the new default when this is enabled. A semantically nullable type
-   * can only be null when there's an error assocaited with the field.
+   * can only be null when there's an error associated with the field.
    *
    * @experimental
    */
@@ -788,6 +788,25 @@ export class Parser {
       type = this.parseNamedType();
     }
 
+    if (this._options.allowSemanticNullability) {
+      if (this.expectOptionalToken(TokenKind.BANG)) {
+        return this.node<NonNullTypeNode>(start, {
+          kind: Kind.NON_NULL_TYPE,
+          type,
+        });
+      } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) {
+        return this.node<SemanticNullableTypeNode>(start, {
+          kind: Kind.SEMANTIC_NULLABLE_TYPE,
+          type,
+        });
+      }
+
+      return this.node<SemanticNonNullTypeNode>(start, {
+        kind: Kind.SEMANTIC_NON_NULL_TYPE,
+        type,
+      });
+    }
+
     if (this.expectOptionalToken(TokenKind.BANG)) {
       return this.node<NonNullTypeNode>(start, {
         kind: Kind.NON_NULL_TYPE,
diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts
index 08273f495f..9b0eaa11a4 100644
--- a/src/type/__tests__/introspection-test.ts
+++ b/src/type/__tests__/introspection-test.ts
@@ -32,7 +32,7 @@ describe('Introspection', () => {
     expect(result).to.deep.equal({
       data: {
         __schema: {
-          queryType: { name: 'SomeObject' },
+          queryType: { kind: 'OBJECT', name: 'SomeObject' },
           mutationType: null,
           subscriptionType: null,
           types: [
diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts
index 774199a368..1c576e8eaa 100644
--- a/src/type/__tests__/predicate-test.ts
+++ b/src/type/__tests__/predicate-test.ts
@@ -765,4 +765,4 @@ describe('Directive predicates', () => {
       expect(isSpecifiedDirective(Directive)).to.equal(false);
     });
   });
-});
\ No newline at end of file
+});
diff --git a/src/type/definition.ts b/src/type/definition.ts
index ff1238793b..80887c852d 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -67,24 +67,24 @@ export type GraphQLType =
       | GraphQLInputObjectType
       | GraphQLList<GraphQLType>
     >
-  |  GraphQLSemanticNonNull<
-    | GraphQLScalarType
-    | GraphQLObjectType
-    | GraphQLInterfaceType
-    | GraphQLUnionType
-    | GraphQLEnumType
-    | GraphQLInputObjectType
-    | GraphQLList<GraphQLType>
-  >
+  | GraphQLSemanticNonNull<
+      | GraphQLScalarType
+      | GraphQLObjectType
+      | GraphQLInterfaceType
+      | GraphQLUnionType
+      | GraphQLEnumType
+      | GraphQLInputObjectType
+      | GraphQLList<GraphQLType>
+    >
   | GraphQLSemanticNullable<
-    | GraphQLScalarType
-    | GraphQLObjectType
-    | GraphQLInterfaceType
-    | GraphQLUnionType
-    | GraphQLEnumType
-    | GraphQLInputObjectType
-    | GraphQLList<GraphQLType>
-  >;
+      | GraphQLScalarType
+      | GraphQLObjectType
+      | GraphQLInterfaceType
+      | GraphQLUnionType
+      | GraphQLEnumType
+      | GraphQLInputObjectType
+      | GraphQLList<GraphQLType>
+    >;
 
 export function isType(type: unknown): type is GraphQLType {
   return (
@@ -325,13 +325,15 @@ export type GraphQLOutputType =
       | GraphQLUnionType
       | GraphQLEnumType
       | GraphQLList<GraphQLOutputType>
-    >   | GraphQLSemanticNonNull<
-    | GraphQLScalarType
-    | GraphQLObjectType
-    | GraphQLInterfaceType
-    | GraphQLUnionType
-    | GraphQLEnumType
-    | GraphQLList<GraphQLOutputType> >;
+    >
+  | GraphQLSemanticNonNull<
+      | GraphQLScalarType
+      | GraphQLObjectType
+      | GraphQLInterfaceType
+      | GraphQLUnionType
+      | GraphQLEnumType
+      | GraphQLList<GraphQLOutputType>
+    >;
 
 export function isOutputType(type: unknown): type is GraphQLOutputType {
   return (
diff --git a/src/type/introspection.ts b/src/type/introspection.ts
index 7dbce27eec..b77ea37380 100644
--- a/src/type/introspection.ts
+++ b/src/type/introspection.ts
@@ -417,10 +417,10 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
           }
 
           const mode =
-          nullability === TypeNullability.AUTO
-            ? info.errorPropagation
-              ? TypeNullability.TRADITIONAL
-              : TypeNullability.SEMANTIC
+            nullability === TypeNullability.AUTO
+              ? info.errorPropagation
+                ? TypeNullability.TRADITIONAL
+                : TypeNullability.SEMANTIC
               : nullability;
           return convertOutputTypeToNullabilityMode(field.type, mode);
         },
diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts
index bcc69557cb..9b0809adf5 100644
--- a/src/utilities/buildClientSchema.ts
+++ b/src/utilities/buildClientSchema.ts
@@ -22,8 +22,8 @@ import {
   GraphQLNonNull,
   GraphQLObjectType,
   GraphQLScalarType,
-  GraphQLUnionType,
   GraphQLSemanticNonNull,
+  GraphQLUnionType,
   isInputType,
   isOutputType,
 } from '../type/definition';
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index 452bc8cdab..40ba62c964 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -53,9 +53,9 @@ import {
   GraphQLNonNull,
   GraphQLObjectType,
   GraphQLScalarType,
-  GraphQLUnionType,
   GraphQLSemanticNonNull,
   GraphQLSemanticNullable,
+  GraphQLUnionType,
   isEnumType,
   isInputObjectType,
   isInterfaceType,
diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts
index c5d5f537a2..9e5bc9b925 100644
--- a/src/utilities/typeFromAST.ts
+++ b/src/utilities/typeFromAST.ts
@@ -11,6 +11,7 @@ import {
   GraphQLList,
   GraphQLNonNull,
   GraphQLSemanticNonNull,
+  GraphQLSemanticNullable,
 } from '../type/definition';
 import type { GraphQLSchema } from '../type/schema';
 
@@ -54,6 +55,10 @@ export function typeFromAST(
       const innerType = typeFromAST(schema, typeNode.type);
       return innerType && new GraphQLSemanticNonNull(innerType);
     }
+    case Kind.SEMANTIC_NULLABLE_TYPE: {
+      const innerType = typeFromAST(schema, typeNode.type);
+      return innerType && new GraphQLSemanticNullable(innerType);
+    }
     case Kind.NAMED_TYPE:
       return schema.getType(typeNode.name.value);
   }

From 869ca46f24446ef152b54165dc866f428aa2dfea Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Wed, 5 Feb 2025 09:17:48 +0100
Subject: [PATCH 03/10] Propose single wrapping type (#4339)

This reduces the new AST-nodes to only be for the newly introduced type,
this does make it so that when we invoke `print` we have to rely on the
user to either specify that we're in semantic nullability mode _or_ we
could do a pre-traverse and when we enter a node with semantic-non-null
we toggle it on ourselves.

The main reasoning behind removing the new name for our existing null
type is that I would prefer to be backwards compatible in terms of
schema structure. This because it might become complex for people to
reason about composed schemas, i.e. a lot of individually parsed schemas
that later on compose into a larger one.

I know that _technically_ this is covered because in the classic ones
we'll have the non wrapped null type and in the modern ones we'll have
the semantic nullable wrapped type. For schema-builders like pothos and
others I think this is rather complex to reason about _and_ to supply us
with. I would instead choose to absorb this complexity in the feature
and stay backwards compatible.

This also sets us up for the SDL not being a breaking change, we only
add one AST-type, what's left now is to settle on a semantic non-null
syntax and making everything backwards compatible.
---
 .../__tests__/semantic-nullability-test.ts    |   3 +-
 src/execution/execute.ts                      |  13 -
 src/language/__tests__/parser-test.ts         |  16 +-
 src/language/__tests__/predicates-test.ts     |   1 -
 src/language/__tests__/schema-printer-test.ts |  20 +-
 src/language/ast.ts                           |  11 +-
 src/language/kinds.ts                         |   1 -
 src/language/parser.ts                        |   6 +-
 src/language/predicates.ts                    |   3 +-
 src/language/printer.ts                       | 597 +++++++++---------
 src/type/definition.ts                        |  91 +--
 src/utilities/extendSchema.ts                 |   4 -
 src/utilities/typeFromAST.ts                  |   5 -
 13 files changed, 332 insertions(+), 439 deletions(-)

diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts
index 20a33c2ffa..6d9098d016 100644
--- a/src/execution/__tests__/semantic-nullability-test.ts
+++ b/src/execution/__tests__/semantic-nullability-test.ts
@@ -10,7 +10,6 @@ import {
   GraphQLNonNull,
   GraphQLObjectType,
   GraphQLSemanticNonNull,
-  GraphQLSemanticNullable,
 } from '../../type/definition';
 import { GraphQLString } from '../../type/scalars';
 import { GraphQLSchema } from '../../type/schema';
@@ -28,7 +27,7 @@ describe('Execute: Handles Semantic Nullability', () => {
   const DataType: GraphQLObjectType = new GraphQLObjectType({
     name: 'DataType',
     fields: () => ({
-      a: { type: new GraphQLSemanticNullable(GraphQLString) },
+      a: { type: GraphQLString },
       b: { type: new GraphQLSemanticNonNull(GraphQLString) },
       c: { type: new GraphQLNonNull(GraphQLString) },
       d: { type: new GraphQLSemanticNonNull(DeepDataType) },
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 0bfbcf3f3e..055b778983 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -44,7 +44,6 @@ import {
   isNonNullType,
   isObjectType,
   isSemanticNonNullType,
-  isSemanticNullableType,
 } from '../type/definition';
 import {
   SchemaMetaFieldDef,
@@ -690,18 +689,6 @@ function completeValue(
     return completed;
   }
 
-  // If field type is SemanticNullable, complete for inner type
-  if (isSemanticNullableType(returnType)) {
-    return completeValue(
-      exeContext,
-      returnType.ofType,
-      fieldNodes,
-      info,
-      path,
-      result,
-    );
-  }
-
   // If result value is null or undefined then return null.
   if (result == null) {
     return null;
diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts
index 4c134f2be0..f3577ef64d 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -659,7 +659,7 @@ describe('Parser', () => {
   });
 
   describe('parseDocumentDirective', () => {
-    it('doesn\'t throw on document-level directive', () => {
+    it("doesn't throw on document-level directive", () => {
       parse(dedent`
         @SemanticNullability
         type Query {
@@ -690,16 +690,12 @@ describe('Parser', () => {
     it('parses nullable types', () => {
       const result = parseType('MyType?', { allowSemanticNullability: true });
       expectJSON(result).toDeepEqual({
-        kind: Kind.SEMANTIC_NULLABLE_TYPE,
-        loc: { start: 0, end: 7 },
-        type: {
-          kind: Kind.NAMED_TYPE,
+        kind: Kind.NAMED_TYPE,
+        loc: { start: 0, end: 6 },
+        name: {
+          kind: Kind.NAME,
           loc: { start: 0, end: 6 },
-          name: {
-            kind: Kind.NAME,
-            loc: { start: 0, end: 6 },
-            value: 'MyType',
-          },
+          value: 'MyType',
         },
       });
     });
diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts
index aa41961177..32ef7d1fe1 100644
--- a/src/language/__tests__/predicates-test.ts
+++ b/src/language/__tests__/predicates-test.ts
@@ -93,7 +93,6 @@ describe('AST node predicates', () => {
       'ListType',
       'NonNullType',
       'SemanticNonNullType',
-      'SemanticNullableType',
     ]);
   });
 
diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts
index a5f803bc1d..a2e3fa070d 100644
--- a/src/language/__tests__/schema-printer-test.ts
+++ b/src/language/__tests__/schema-printer-test.ts
@@ -183,28 +183,38 @@ describe('Printer: SDL document', () => {
 
   it('prints NamedType', () => {
     expect(
-      print(parseType('MyType', { allowSemanticNullability: false })),
+      print(parseType('MyType', { allowSemanticNullability: false }), {
+        useSemanticNullability: false,
+      }),
     ).to.equal(dedent`MyType`);
   });
 
   it('prints SemanticNullableType', () => {
     expect(
-      print(parseType('MyType?', { allowSemanticNullability: true })),
+      print(parseType('MyType?', { allowSemanticNullability: true }), {
+        useSemanticNullability: true,
+      }),
     ).to.equal(dedent`MyType?`);
   });
 
   it('prints SemanticNonNullType', () => {
     expect(
-      print(parseType('MyType', { allowSemanticNullability: true })),
+      print(parseType('MyType', { allowSemanticNullability: true }), {
+        useSemanticNullability: true,
+      }),
     ).to.equal(dedent`MyType`);
   });
 
   it('prints NonNullType', () => {
     expect(
-      print(parseType('MyType!', { allowSemanticNullability: true })),
+      print(parseType('MyType!', { allowSemanticNullability: true }), {
+        useSemanticNullability: true,
+      }),
     ).to.equal(dedent`MyType!`);
     expect(
-      print(parseType('MyType!', { allowSemanticNullability: false })),
+      print(parseType('MyType!', { allowSemanticNullability: false }), {
+        useSemanticNullability: true,
+      }),
     ).to.equal(dedent`MyType!`);
   });
 });
diff --git a/src/language/ast.ts b/src/language/ast.ts
index 57beb3c573..21c4160464 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -162,7 +162,6 @@ export type ASTNode =
   | ListTypeNode
   | NonNullTypeNode
   | SemanticNonNullTypeNode
-  | SemanticNullableTypeNode
   | SchemaDefinitionNode
   | OperationTypeDefinitionNode
   | ScalarTypeDefinitionNode
@@ -238,7 +237,6 @@ export const QueryDocumentKeys: {
   ListType: ['type'],
   NonNullType: ['type'],
   SemanticNonNullType: ['type'],
-  SemanticNullableType: ['type'],
 
   SchemaDefinition: ['description', 'directives', 'operationTypes'],
   OperationTypeDefinition: ['type'],
@@ -529,20 +527,13 @@ export interface SemanticNonNullTypeNode {
   readonly type: NamedTypeNode | ListTypeNode;
 }
 
-export interface SemanticNullableTypeNode {
-  readonly kind: Kind.SEMANTIC_NULLABLE_TYPE;
-  readonly loc?: Location;
-  readonly type: NamedTypeNode | ListTypeNode;
-}
-
 /** Type Reference */
 
 export type TypeNode =
   | NamedTypeNode
   | ListTypeNode
   | NonNullTypeNode
-  | SemanticNonNullTypeNode
-  | SemanticNullableTypeNode;
+  | SemanticNonNullTypeNode;
 
 export interface NamedTypeNode {
   readonly kind: Kind.NAMED_TYPE;
diff --git a/src/language/kinds.ts b/src/language/kinds.ts
index 7111a94834..e91373746c 100644
--- a/src/language/kinds.ts
+++ b/src/language/kinds.ts
@@ -38,7 +38,6 @@ enum Kind {
   LIST_TYPE = 'ListType',
   NON_NULL_TYPE = 'NonNullType',
   SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType',
-  SEMANTIC_NULLABLE_TYPE = 'SemanticNullableType',
 
   /** Type System Definitions */
   SCHEMA_DEFINITION = 'SchemaDefinition',
diff --git a/src/language/parser.ts b/src/language/parser.ts
index 5743eef9da..790aee3b4b 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -51,7 +51,6 @@ import type {
   SelectionNode,
   SelectionSetNode,
   SemanticNonNullTypeNode,
-  SemanticNullableTypeNode,
   StringValueNode,
   Token,
   TypeNode,
@@ -795,10 +794,7 @@ export class Parser {
           type,
         });
       } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) {
-        return this.node<SemanticNullableTypeNode>(start, {
-          kind: Kind.SEMANTIC_NULLABLE_TYPE,
-          type,
-        });
+        return type;
       }
 
       return this.node<SemanticNonNullTypeNode>(start, {
diff --git a/src/language/predicates.ts b/src/language/predicates.ts
index d528e6c3c2..3ddf52b94c 100644
--- a/src/language/predicates.ts
+++ b/src/language/predicates.ts
@@ -68,8 +68,7 @@ export function isTypeNode(node: ASTNode): node is TypeNode {
     node.kind === Kind.NAMED_TYPE ||
     node.kind === Kind.LIST_TYPE ||
     node.kind === Kind.NON_NULL_TYPE ||
-    node.kind === Kind.SEMANTIC_NON_NULL_TYPE ||
-    node.kind === Kind.SEMANTIC_NULLABLE_TYPE
+    node.kind === Kind.SEMANTIC_NON_NULL_TYPE
   );
 }
 
diff --git a/src/language/printer.ts b/src/language/printer.ts
index 17b805e624..2b14ec5dfd 100644
--- a/src/language/printer.ts
+++ b/src/language/printer.ts
@@ -2,6 +2,7 @@ import type { Maybe } from '../jsutils/Maybe';
 
 import type { ASTNode } from './ast';
 import { printBlockString } from './blockString';
+import { Kind } from './kinds';
 import { printString } from './printString';
 import type { ASTReducer } from './visitor';
 import { visit } from './visitor';
@@ -17,302 +18,314 @@ export interface PrintOptions {
  * Converts an AST into a string, using one set of reasonable
  * formatting rules.
  */
-export function print(ast: ASTNode): string {
-  return visit(ast, printDocASTReducer);
-}
+export function print(ast: ASTNode, options: PrintOptions = {}): string {
+  return visit<string>(ast, {
+    Name: { leave: (node) => node.value },
+    Variable: { leave: (node) => '$' + node.name },
 
-const MAX_LINE_LENGTH = 80;
+    // Document
 
-const printDocASTReducer: ASTReducer<string> = {
-  Name: { leave: (node) => node.value },
-  Variable: { leave: (node) => '$' + node.name },
-
-  // Document
-
-  Document: {
-    leave: (node) => join(node.definitions, '\n\n'),
-  },
-
-  OperationDefinition: {
-    leave(node) {
-      const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')');
-      const prefix = join(
-        [
-          node.operation,
-          join([node.name, varDefs]),
-          join(node.directives, ' '),
-        ],
-        ' ',
-      );
-
-      // Anonymous queries with no directives or variable definitions can use
-      // the query short form.
-      return (prefix === 'query' ? '' : prefix + ' ') + node.selectionSet;
+    Document: {
+      leave: (node) => join(node.definitions, '\n\n'),
     },
-  },
-
-  VariableDefinition: {
-    leave: ({ variable, type, defaultValue, directives }) =>
-      variable +
-      ': ' +
-      type +
-      wrap(' = ', defaultValue) +
-      wrap(' ', join(directives, ' ')),
-  },
-  SelectionSet: { leave: ({ selections }) => block(selections) },
-
-  Field: {
-    leave({ alias, name, arguments: args, directives, selectionSet }) {
-      const prefix = wrap('', alias, ': ') + name;
-      let argsLine = prefix + wrap('(', join(args, ', '), ')');
-
-      if (argsLine.length > MAX_LINE_LENGTH) {
-        argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
-      }
-
-      return join([argsLine, join(directives, ' '), selectionSet], ' ');
+
+    OperationDefinition: {
+      leave(node) {
+        const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')');
+        const prefix = join(
+          [
+            node.operation,
+            join([node.name, varDefs]),
+            join(node.directives, ' '),
+          ],
+          ' ',
+        );
+
+        // Anonymous queries with no directives or variable definitions can use
+        // the query short form.
+        return (prefix === 'query' ? '' : prefix + ' ') + node.selectionSet;
+      },
+    },
+
+    VariableDefinition: {
+      leave: ({ variable, type, defaultValue, directives }) =>
+        variable +
+        ': ' +
+        type +
+        wrap(' = ', defaultValue) +
+        wrap(' ', join(directives, ' ')),
+    },
+    SelectionSet: { leave: ({ selections }) => block(selections) },
+
+    Field: {
+      leave({ alias, name, arguments: args, directives, selectionSet }) {
+        const prefix = wrap('', alias, ': ') + name;
+        let argsLine = prefix + wrap('(', join(args, ', '), ')');
+
+        if (argsLine.length > MAX_LINE_LENGTH) {
+          argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
+        }
+
+        return join([argsLine, join(directives, ' '), selectionSet], ' ');
+      },
+    },
+
+    Argument: { leave: ({ name, value }) => name + ': ' + value },
+
+    // Fragments
+
+    FragmentSpread: {
+      leave: ({ name, directives }) =>
+        '...' + name + wrap(' ', join(directives, ' ')),
+    },
+
+    InlineFragment: {
+      leave: ({ typeCondition, directives, selectionSet }) =>
+        join(
+          [
+            '...',
+            wrap('on ', typeCondition),
+            join(directives, ' '),
+            selectionSet,
+          ],
+          ' ',
+        ),
+    },
+
+    FragmentDefinition: {
+      leave: ({
+        name,
+        typeCondition,
+        variableDefinitions,
+        directives,
+        selectionSet,
+      }) =>
+        // Note: fragment variable definitions are experimental and may be changed
+        // or removed in the future.
+        `fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` +
+        `on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` +
+        selectionSet,
     },
-  },
-
-  Argument: { leave: ({ name, value }) => name + ': ' + value },
-
-  // Fragments
-
-  FragmentSpread: {
-    leave: ({ name, directives }) =>
-      '...' + name + wrap(' ', join(directives, ' ')),
-  },
-
-  InlineFragment: {
-    leave: ({ typeCondition, directives, selectionSet }) =>
-      join(
-        [
-          '...',
-          wrap('on ', typeCondition),
-          join(directives, ' '),
-          selectionSet,
-        ],
-        ' ',
-      ),
-  },
-
-  FragmentDefinition: {
-    leave: ({
-      name,
-      typeCondition,
-      variableDefinitions,
-      directives,
-      selectionSet,
-    }) =>
-      // Note: fragment variable definitions are experimental and may be changed
-      // or removed in the future.
-      `fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` +
-      `on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` +
-      selectionSet,
-  },
-
-  // Value
-
-  IntValue: { leave: ({ value }) => value },
-  FloatValue: { leave: ({ value }) => value },
-  StringValue: {
-    leave: ({ value, block: isBlockString }) =>
-      isBlockString ? printBlockString(value) : printString(value),
-  },
-  BooleanValue: { leave: ({ value }) => (value ? 'true' : 'false') },
-  NullValue: { leave: () => 'null' },
-  EnumValue: { leave: ({ value }) => value },
-  ListValue: { leave: ({ values }) => '[' + join(values, ', ') + ']' },
-  ObjectValue: { leave: ({ fields }) => '{' + join(fields, ', ') + '}' },
-  ObjectField: { leave: ({ name, value }) => name + ': ' + value },
-
-  // Directive
-
-  Directive: {
-    leave: ({ name, arguments: args }) =>
-      '@' + name + wrap('(', join(args, ', '), ')'),
-  },
-
-  // Type
-
-  NamedType: { leave: ({ name }) => name },
-  ListType: { leave: ({ type }) => '[' + type + ']' },
-  NonNullType: { leave: ({ type }) => type + '!' },
-  SemanticNonNullType: { leave: ({ type }) => type },
-  SemanticNullableType: { leave: ({ type }) => type + '?' },
-
-  // Type System Definitions
-
-  SchemaDefinition: {
-    leave: ({ description, directives, operationTypes }) =>
-      wrap('', description, '\n') +
-      join(['schema', join(directives, ' '), block(operationTypes)], ' '),
-  },
-
-  OperationTypeDefinition: {
-    leave: ({ operation, type }) => operation + ': ' + type,
-  },
-
-  ScalarTypeDefinition: {
-    leave: ({ description, name, directives }) =>
-      wrap('', description, '\n') +
-      join(['scalar', name, join(directives, ' ')], ' '),
-  },
-
-  ObjectTypeDefinition: {
-    leave: ({ description, name, interfaces, directives, fields }) =>
-      wrap('', description, '\n') +
-      join(
-        [
-          'type',
-          name,
-          wrap('implements ', join(interfaces, ' & ')),
-          join(directives, ' '),
-          block(fields),
-        ],
-        ' ',
-      ),
-  },
-
-  FieldDefinition: {
-    leave: ({ description, name, arguments: args, type, directives }) =>
-      wrap('', description, '\n') +
-      name +
-      (hasMultilineItems(args)
-        ? wrap('(\n', indent(join(args, '\n')), '\n)')
-        : wrap('(', join(args, ', '), ')')) +
-      ': ' +
-      type +
-      wrap(' ', join(directives, ' ')),
-  },
-
-  InputValueDefinition: {
-    leave: ({ description, name, type, defaultValue, directives }) =>
-      wrap('', description, '\n') +
-      join(
-        [name + ': ' + type, wrap('= ', defaultValue), join(directives, ' ')],
-        ' ',
-      ),
-  },
-
-  InterfaceTypeDefinition: {
-    leave: ({ description, name, interfaces, directives, fields }) =>
-      wrap('', description, '\n') +
-      join(
-        [
-          'interface',
-          name,
-          wrap('implements ', join(interfaces, ' & ')),
-          join(directives, ' '),
-          block(fields),
-        ],
-        ' ',
-      ),
-  },
-
-  UnionTypeDefinition: {
-    leave: ({ description, name, directives, types }) =>
-      wrap('', description, '\n') +
-      join(
-        ['union', name, join(directives, ' '), wrap('= ', join(types, ' | '))],
-        ' ',
-      ),
-  },
-
-  EnumTypeDefinition: {
-    leave: ({ description, name, directives, values }) =>
-      wrap('', description, '\n') +
-      join(['enum', name, join(directives, ' '), block(values)], ' '),
-  },
-
-  EnumValueDefinition: {
-    leave: ({ description, name, directives }) =>
-      wrap('', description, '\n') + join([name, join(directives, ' ')], ' '),
-  },
-
-  InputObjectTypeDefinition: {
-    leave: ({ description, name, directives, fields }) =>
-      wrap('', description, '\n') +
-      join(['input', name, join(directives, ' '), block(fields)], ' '),
-  },
-
-  DirectiveDefinition: {
-    leave: ({ description, name, arguments: args, repeatable, locations }) =>
-      wrap('', description, '\n') +
-      'directive @' +
-      name +
-      (hasMultilineItems(args)
-        ? wrap('(\n', indent(join(args, '\n')), '\n)')
-        : wrap('(', join(args, ', '), ')')) +
-      (repeatable ? ' repeatable' : '') +
-      ' on ' +
-      join(locations, ' | '),
-  },
-
-  SchemaExtension: {
-    leave: ({ directives, operationTypes }) =>
-      join(
-        ['extend schema', join(directives, ' '), block(operationTypes)],
-        ' ',
-      ),
-  },
-
-  ScalarTypeExtension: {
-    leave: ({ name, directives }) =>
-      join(['extend scalar', name, join(directives, ' ')], ' '),
-  },
-
-  ObjectTypeExtension: {
-    leave: ({ name, interfaces, directives, fields }) =>
-      join(
-        [
-          'extend type',
-          name,
-          wrap('implements ', join(interfaces, ' & ')),
-          join(directives, ' '),
-          block(fields),
-        ],
-        ' ',
-      ),
-  },
-
-  InterfaceTypeExtension: {
-    leave: ({ name, interfaces, directives, fields }) =>
-      join(
-        [
-          'extend interface',
-          name,
-          wrap('implements ', join(interfaces, ' & ')),
-          join(directives, ' '),
-          block(fields),
-        ],
-        ' ',
-      ),
-  },
-
-  UnionTypeExtension: {
-    leave: ({ name, directives, types }) =>
-      join(
-        [
-          'extend union',
-          name,
-          join(directives, ' '),
-          wrap('= ', join(types, ' | ')),
-        ],
-        ' ',
-      ),
-  },
-
-  EnumTypeExtension: {
-    leave: ({ name, directives, values }) =>
-      join(['extend enum', name, join(directives, ' '), block(values)], ' '),
-  },
-
-  InputObjectTypeExtension: {
-    leave: ({ name, directives, fields }) =>
-      join(['extend input', name, join(directives, ' '), block(fields)], ' '),
-  },
-};
+
+    // Value
+
+    IntValue: { leave: ({ value }) => value },
+    FloatValue: { leave: ({ value }) => value },
+    StringValue: {
+      leave: ({ value, block: isBlockString }) =>
+        isBlockString ? printBlockString(value) : printString(value),
+    },
+    BooleanValue: { leave: ({ value }) => (value ? 'true' : 'false') },
+    NullValue: { leave: () => 'null' },
+    EnumValue: { leave: ({ value }) => value },
+    ListValue: { leave: ({ values }) => '[' + join(values, ', ') + ']' },
+    ObjectValue: { leave: ({ fields }) => '{' + join(fields, ', ') + '}' },
+    ObjectField: { leave: ({ name, value }) => name + ': ' + value },
+
+    // Directive
+
+    Directive: {
+      leave: ({ name, arguments: args }) =>
+        '@' + name + wrap('(', join(args, ', '), ')'),
+    },
+
+    // Type
+
+    NamedType: {
+      leave: ({ name }, _, parent) =>
+        parent &&
+        !Array.isArray(parent) &&
+        ((parent as ASTNode).kind === Kind.SEMANTIC_NON_NULL_TYPE ||
+          (parent as ASTNode).kind === Kind.NON_NULL_TYPE)
+          ? name
+          : options?.useSemanticNullability
+          ? `${name}?`
+          : name,
+    },
+    ListType: { leave: ({ type }) => '[' + type + ']' },
+    NonNullType: { leave: ({ type }) => type + '!' },
+    SemanticNonNullType: { leave: ({ type }) => type },
+
+    // Type System Definitions
+
+    SchemaDefinition: {
+      leave: ({ description, directives, operationTypes }) =>
+        wrap('', description, '\n') +
+        join(['schema', join(directives, ' '), block(operationTypes)], ' '),
+    },
+
+    OperationTypeDefinition: {
+      leave: ({ operation, type }) => operation + ': ' + type,
+    },
+
+    ScalarTypeDefinition: {
+      leave: ({ description, name, directives }) =>
+        wrap('', description, '\n') +
+        join(['scalar', name, join(directives, ' ')], ' '),
+    },
+
+    ObjectTypeDefinition: {
+      leave: ({ description, name, interfaces, directives, fields }) =>
+        wrap('', description, '\n') +
+        join(
+          [
+            'type',
+            name,
+            wrap('implements ', join(interfaces, ' & ')),
+            join(directives, ' '),
+            block(fields),
+          ],
+          ' ',
+        ),
+    },
+
+    FieldDefinition: {
+      leave: ({ description, name, arguments: args, type, directives }) =>
+        wrap('', description, '\n') +
+        name +
+        (hasMultilineItems(args)
+          ? wrap('(\n', indent(join(args, '\n')), '\n)')
+          : wrap('(', join(args, ', '), ')')) +
+        ': ' +
+        type +
+        wrap(' ', join(directives, ' ')),
+    },
+
+    InputValueDefinition: {
+      leave: ({ description, name, type, defaultValue, directives }) =>
+        wrap('', description, '\n') +
+        join(
+          [name + ': ' + type, wrap('= ', defaultValue), join(directives, ' ')],
+          ' ',
+        ),
+    },
+
+    InterfaceTypeDefinition: {
+      leave: ({ description, name, interfaces, directives, fields }) =>
+        wrap('', description, '\n') +
+        join(
+          [
+            'interface',
+            name,
+            wrap('implements ', join(interfaces, ' & ')),
+            join(directives, ' '),
+            block(fields),
+          ],
+          ' ',
+        ),
+    },
+
+    UnionTypeDefinition: {
+      leave: ({ description, name, directives, types }) =>
+        wrap('', description, '\n') +
+        join(
+          [
+            'union',
+            name,
+            join(directives, ' '),
+            wrap('= ', join(types, ' | ')),
+          ],
+          ' ',
+        ),
+    },
+
+    EnumTypeDefinition: {
+      leave: ({ description, name, directives, values }) =>
+        wrap('', description, '\n') +
+        join(['enum', name, join(directives, ' '), block(values)], ' '),
+    },
+
+    EnumValueDefinition: {
+      leave: ({ description, name, directives }) =>
+        wrap('', description, '\n') + join([name, join(directives, ' ')], ' '),
+    },
+
+    InputObjectTypeDefinition: {
+      leave: ({ description, name, directives, fields }) =>
+        wrap('', description, '\n') +
+        join(['input', name, join(directives, ' '), block(fields)], ' '),
+    },
+
+    DirectiveDefinition: {
+      leave: ({ description, name, arguments: args, repeatable, locations }) =>
+        wrap('', description, '\n') +
+        'directive @' +
+        name +
+        (hasMultilineItems(args)
+          ? wrap('(\n', indent(join(args, '\n')), '\n)')
+          : wrap('(', join(args, ', '), ')')) +
+        (repeatable ? ' repeatable' : '') +
+        ' on ' +
+        join(locations, ' | '),
+    },
+
+    SchemaExtension: {
+      leave: ({ directives, operationTypes }) =>
+        join(
+          ['extend schema', join(directives, ' '), block(operationTypes)],
+          ' ',
+        ),
+    },
+
+    ScalarTypeExtension: {
+      leave: ({ name, directives }) =>
+        join(['extend scalar', name, join(directives, ' ')], ' '),
+    },
+
+    ObjectTypeExtension: {
+      leave: ({ name, interfaces, directives, fields }) =>
+        join(
+          [
+            'extend type',
+            name,
+            wrap('implements ', join(interfaces, ' & ')),
+            join(directives, ' '),
+            block(fields),
+          ],
+          ' ',
+        ),
+    },
+
+    InterfaceTypeExtension: {
+      leave: ({ name, interfaces, directives, fields }) =>
+        join(
+          [
+            'extend interface',
+            name,
+            wrap('implements ', join(interfaces, ' & ')),
+            join(directives, ' '),
+            block(fields),
+          ],
+          ' ',
+        ),
+    },
+
+    UnionTypeExtension: {
+      leave: ({ name, directives, types }) =>
+        join(
+          [
+            'extend union',
+            name,
+            join(directives, ' '),
+            wrap('= ', join(types, ' | ')),
+          ],
+          ' ',
+        ),
+    },
+
+    EnumTypeExtension: {
+      leave: ({ name, directives, values }) =>
+        join(['extend enum', name, join(directives, ' '), block(values)], ' '),
+    },
+
+    InputObjectTypeExtension: {
+      leave: ({ name, directives, fields }) =>
+        join(['extend input', name, join(directives, ' '), block(fields)], ' '),
+    },
+  });
+}
+
+const MAX_LINE_LENGTH = 80;
 
 /**
  * Given maybeArray, print an empty string if it is null or empty, otherwise
diff --git a/src/type/definition.ts b/src/type/definition.ts
index 80887c852d..bcd3862c89 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -75,15 +75,6 @@ export type GraphQLType =
       | GraphQLEnumType
       | GraphQLInputObjectType
       | GraphQLList<GraphQLType>
-    >
-  | GraphQLSemanticNullable<
-      | GraphQLScalarType
-      | GraphQLObjectType
-      | GraphQLInterfaceType
-      | GraphQLUnionType
-      | GraphQLEnumType
-      | GraphQLInputObjectType
-      | GraphQLList<GraphQLType>
     >;
 
 export function isType(type: unknown): type is GraphQLType {
@@ -249,32 +240,6 @@ export function assertSemanticNonNullType(
   return type;
 }
 
-export function isSemanticNullableType(
-  type: GraphQLInputType,
-): type is GraphQLSemanticNullable<GraphQLInputType>;
-export function isSemanticNullableType(
-  type: GraphQLOutputType,
-): type is GraphQLSemanticNullable<GraphQLOutputType>;
-export function isSemanticNullableType(
-  type: unknown,
-): type is GraphQLSemanticNullable<GraphQLType>;
-export function isSemanticNullableType(
-  type: unknown,
-): type is GraphQLSemanticNullable<GraphQLType> {
-  return instanceOf(type, GraphQLSemanticNullable);
-}
-
-export function assertSemanticNullableType(
-  type: unknown,
-): GraphQLSemanticNullable<GraphQLType> {
-  if (!isSemanticNullableType(type)) {
-    throw new Error(
-      `Expected ${inspect(type)} to be a GraphQL Semantic-Non-Null type.`,
-    );
-  }
-  return type;
-}
-
 /**
  * These types may be used as input types for arguments and directives.
  */
@@ -545,52 +510,6 @@ export class GraphQLSemanticNonNull<T extends GraphQLNullableType> {
   }
 }
 
-/**
- * Semantic-Nullable Type Wrapper
- *
- * A semantic-nullable is a wrapping type which points to another type.
- * Semantic-nullable types allow their values to be null.
- *
- * Example:
- *
- * ```ts
- * const RowType = new GraphQLObjectType({
- *   name: 'Row',
- *   fields: () => ({
- *     email: { type: new GraphQLSemanticNullable(GraphQLString) },
- *   })
- * })
- * ```
- * Note: This is equivalent to the unadorned named type that is
- * used by GraphQL when it is not operating in SemanticNullability mode.
- *
- * @experimental
- */
-export class GraphQLSemanticNullable<T extends GraphQLNullableType> {
-  readonly ofType: T;
-
-  constructor(ofType: T) {
-    devAssert(
-      isNullableType(ofType),
-      `Expected ${inspect(ofType)} to be a GraphQL nullable type.`,
-    );
-
-    this.ofType = ofType;
-  }
-
-  get [Symbol.toStringTag]() {
-    return 'GraphQLSemanticNullable';
-  }
-
-  toString(): string {
-    return String(this.ofType) + '?';
-  }
-
-  toJSON(): string {
-    return this.toString();
-  }
-}
-
 /**
  * These types wrap and modify other types
  */
@@ -598,16 +517,10 @@ export class GraphQLSemanticNullable<T extends GraphQLNullableType> {
 export type GraphQLWrappingType =
   | GraphQLList<GraphQLType>
   | GraphQLNonNull<GraphQLType>
-  | GraphQLSemanticNonNull<GraphQLType>
-  | GraphQLSemanticNullable<GraphQLType>;
+  | GraphQLSemanticNonNull<GraphQLType>;
 
 export function isWrappingType(type: unknown): type is GraphQLWrappingType {
-  return (
-    isListType(type) ||
-    isNonNullType(type) ||
-    isSemanticNonNullType(type) ||
-    isSemanticNullableType(type)
-  );
+  return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type);
 }
 
 export function assertWrappingType(type: unknown): GraphQLWrappingType {
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index 40ba62c964..876aae277f 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -54,7 +54,6 @@ import {
   GraphQLObjectType,
   GraphQLScalarType,
   GraphQLSemanticNonNull,
-  GraphQLSemanticNullable,
   GraphQLUnionType,
   isEnumType,
   isInputObjectType,
@@ -442,9 +441,6 @@ export function extendSchemaImpl(
     if (node.kind === Kind.SEMANTIC_NON_NULL_TYPE) {
       return new GraphQLSemanticNonNull(getWrappedType(node.type));
     }
-    if (node.kind === Kind.SEMANTIC_NULLABLE_TYPE) {
-      return new GraphQLSemanticNullable(getWrappedType(node.type));
-    }
     return getNamedType(node);
   }
 
diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts
index 9e5bc9b925..c5d5f537a2 100644
--- a/src/utilities/typeFromAST.ts
+++ b/src/utilities/typeFromAST.ts
@@ -11,7 +11,6 @@ import {
   GraphQLList,
   GraphQLNonNull,
   GraphQLSemanticNonNull,
-  GraphQLSemanticNullable,
 } from '../type/definition';
 import type { GraphQLSchema } from '../type/schema';
 
@@ -55,10 +54,6 @@ export function typeFromAST(
       const innerType = typeFromAST(schema, typeNode.type);
       return innerType && new GraphQLSemanticNonNull(innerType);
     }
-    case Kind.SEMANTIC_NULLABLE_TYPE: {
-      const innerType = typeFromAST(schema, typeNode.type);
-      return innerType && new GraphQLSemanticNullable(innerType);
-    }
     case Kind.NAMED_TYPE:
       return schema.getType(typeNode.name.value);
   }

From e0c242586c31a91d1f2542da8ea384036c4fba17 Mon Sep 17 00:00:00 2001
From: jdecroock <decroockjovi@gmail.com>
Date: Wed, 5 Feb 2025 16:45:22 +0100
Subject: [PATCH 04/10] Fix linting

---
 src/language/printer.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/language/printer.ts b/src/language/printer.ts
index 2b14ec5dfd..66d591d619 100644
--- a/src/language/printer.ts
+++ b/src/language/printer.ts
@@ -4,7 +4,6 @@ import type { ASTNode } from './ast';
 import { printBlockString } from './blockString';
 import { Kind } from './kinds';
 import { printString } from './printString';
-import type { ASTReducer } from './visitor';
 import { visit } from './visitor';
 
 /**

From 7121006dd42fdf6e4ba8847bad97a11015b480a6 Mon Sep 17 00:00:00 2001
From: Alex Reilly <fabiobean2@gmail.com>
Date: Thu, 6 Feb 2025 23:17:42 -0800
Subject: [PATCH 05/10] fixed tests that were using print (#4341)

There were a few tests that broke due to changes to print params
---
 .../__tests__/buildASTSchema-test.ts          |  2 +-
 src/utilities/__tests__/extendSchema-test.ts  |  8 +++++---
 .../__tests__/separateOperations-test.ts      | 20 ++++++++++++++-----
 3 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts
index 29280474ec..a3e23affe9 100644
--- a/src/utilities/__tests__/buildASTSchema-test.ts
+++ b/src/utilities/__tests__/buildASTSchema-test.ts
@@ -60,7 +60,7 @@ function expectASTNode(obj: Maybe<{ readonly astNode: Maybe<ASTNode> }>) {
 function expectExtensionASTNodes(obj: {
   readonly extensionASTNodes: ReadonlyArray<ASTNode>;
 }) {
-  return expect(obj.extensionASTNodes.map(print).join('\n\n'));
+  return expect(obj.extensionASTNodes.map((node) => print(node)).join('\n\n'));
 }
 
 describe('Schema Builder', () => {
diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts
index 86baf0e699..171b0e6b62 100644
--- a/src/utilities/__tests__/extendSchema-test.ts
+++ b/src/utilities/__tests__/extendSchema-test.ts
@@ -39,7 +39,7 @@ import { printSchema } from '../printSchema';
 function expectExtensionASTNodes(obj: {
   readonly extensionASTNodes: ReadonlyArray<ASTNode>;
 }) {
-  return expect(obj.extensionASTNodes.map(print).join('\n\n'));
+  return expect(obj.extensionASTNodes.map((node) => print(node)).join('\n\n'));
 }
 
 function expectASTNode(obj: Maybe<{ readonly astNode: Maybe<ASTNode> }>) {
@@ -51,10 +51,12 @@ function expectSchemaChanges(
   schema: GraphQLSchema,
   extendedSchema: GraphQLSchema,
 ) {
-  const schemaDefinitions = parse(printSchema(schema)).definitions.map(print);
+  const schemaDefinitions = parse(printSchema(schema)).definitions.map((node) =>
+    print(node),
+  );
   return expect(
     parse(printSchema(extendedSchema))
-      .definitions.map(print)
+      .definitions.map((node) => print(node))
       .filter((def) => !schemaDefinitions.includes(def))
       .join('\n\n'),
   );
diff --git a/src/utilities/__tests__/separateOperations-test.ts b/src/utilities/__tests__/separateOperations-test.ts
index 2f14bae9ac..aacf7bc15f 100644
--- a/src/utilities/__tests__/separateOperations-test.ts
+++ b/src/utilities/__tests__/separateOperations-test.ts
@@ -49,7 +49,9 @@ describe('separateOperations', () => {
       }
     `);
 
-    const separatedASTs = mapValue(separateOperations(ast), print);
+    const separatedASTs = mapValue(separateOperations(ast), (node) =>
+      print(node),
+    );
     expect(separatedASTs).to.deep.equal({
       '': dedent`
         {
@@ -128,7 +130,9 @@ describe('separateOperations', () => {
       }
     `);
 
-    const separatedASTs = mapValue(separateOperations(ast), print);
+    const separatedASTs = mapValue(separateOperations(ast), (node) =>
+      print(node),
+    );
     expect(separatedASTs).to.deep.equal({
       One: dedent`
         query One {
@@ -178,7 +182,9 @@ describe('separateOperations', () => {
       }
     `);
 
-    const separatedASTs = mapValue(separateOperations(ast), print);
+    const separatedASTs = mapValue(separateOperations(ast), (node) =>
+      print(node),
+    );
     expect(separatedASTs).to.deep.equal({
       '': dedent`
         {
@@ -215,7 +221,9 @@ describe('separateOperations', () => {
       type Bar
     `);
 
-    const separatedASTs = mapValue(separateOperations(ast), print);
+    const separatedASTs = mapValue(separateOperations(ast), (node) =>
+      print(node),
+    );
     expect(separatedASTs).to.deep.equal({
       Foo: dedent`
         query Foo {
@@ -241,7 +249,9 @@ describe('separateOperations', () => {
       }
     `);
 
-    const separatedASTs = mapValue(separateOperations(ast), print);
+    const separatedASTs = mapValue(separateOperations(ast), (node) =>
+      print(node),
+    );
     expect(separatedASTs).to.deep.equal({
       '': dedent`
         {

From 2321f932952e81d736cb9f8c24f3d9ac8e9010ce Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Wed, 12 Feb 2025 03:54:43 +0100
Subject: [PATCH 06/10] Cleanup and code coverage

---
 .../__tests__/semantic-nullability-test.ts    |  26 ----
 src/type/__tests__/introspection-test.ts      | 116 ++++++++++++--
 src/type/directives.ts                        |  11 ++
 src/type/introspection.ts                     |  47 ++----
 src/utilities/__tests__/TypeInfo-test.ts      |  62 ++++++++
 .../__tests__/buildClientSchema-test.ts       |  60 ++++++++
 src/utilities/__tests__/extendSchema-test.ts  |  36 ++++-
 .../__tests__/findBreakingChanges-test.ts     | 100 ++++++++++++
 .../__tests__/getIntrospectionQuery-test.ts   |   9 ++
 .../__tests__/lexicographicSortSchema-test.ts |  54 +++++++
 src/utilities/__tests__/printSchema-test.ts   |  16 +-
 .../__tests__/typeComparators-test.ts         |  54 +++++++
 src/utilities/buildClientSchema.ts            |   1 +
 src/utilities/getIntrospectionQuery.ts        |   8 +-
 src/utilities/printSchema.ts                  |  44 ++++--
 src/utilities/typeComparators.ts              |   2 +-
 src/utilities/typeFromAST.ts                  |  10 +-
 .../OverlappingFieldsCanBeMergedRule-test.ts  | 145 ++++++++++++++++++
 18 files changed, 698 insertions(+), 103 deletions(-)

diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts
index 6d9098d016..613ab91d1c 100644
--- a/src/execution/__tests__/semantic-nullability-test.ts
+++ b/src/execution/__tests__/semantic-nullability-test.ts
@@ -165,32 +165,6 @@ describe('Execute: Handles Semantic Nullability', () => {
     });
   });
 
-  it('SemanticNullable allows null values', async () => {
-    const data = {
-      a: () => null,
-      b: () => null,
-      c: () => 'Cookie',
-    };
-
-    const document = parse(`
-        query {
-          a
-        }
-      `);
-
-    const result = await execute({
-      schema: new GraphQLSchema({ query: DataType }),
-      document,
-      rootValue: data,
-    });
-
-    expect(result).to.deep.equal({
-      data: {
-        a: null,
-      },
-    });
-  });
-
   it('SemanticNullable allows non-null values', async () => {
     const data = {
       a: () => 'Apple',
diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts
index 9b0eaa11a4..09c12abb06 100644
--- a/src/type/__tests__/introspection-test.ts
+++ b/src/type/__tests__/introspection-test.ts
@@ -523,7 +523,7 @@ describe('Introspection', () => {
                           ofType: null,
                         },
                       },
-                      defaultValue: 'AUTO',
+                      defaultValue: 'TRADITIONAL',
                     },
                   ],
                   type: {
@@ -667,21 +667,11 @@ describe('Introspection', () => {
               inputFields: null,
               interfaces: null,
               enumValues: [
-                {
-                  name: 'AUTO',
-                  isDeprecated: false,
-                  deprecationReason: null,
-                },
                 {
                   name: 'TRADITIONAL',
                   isDeprecated: false,
                   deprecationReason: null,
                 },
-                {
-                  name: 'SEMANTIC',
-                  isDeprecated: false,
-                  deprecationReason: null,
-                },
                 {
                   name: 'FULL',
                   isDeprecated: false,
@@ -1804,4 +1794,108 @@ describe('Introspection', () => {
     });
     expect(result).to.not.have.property('errors');
   });
+
+  describe('semantic nullability', () => {
+    it('casts semantic-non-null types to nullable types in traditional mode', () => {
+      const schema = buildSchema(`
+        @SemanticNullability
+        type Query {
+          someField: String!
+          someField2: String
+          someField3: String?
+        }
+      `);
+
+      const source = getIntrospectionQuery({
+        nullability: 'TRADITIONAL',
+      });
+
+      const result = graphqlSync({ schema, source });
+      // @ts-expect-error
+      const queryType = result.data?.__schema?.types.find(
+        // @ts-expect-error
+        (t) => t.name === 'Query',
+      );
+      const defaults = {
+        args: [],
+        deprecationReason: null,
+        description: null,
+        isDeprecated: false,
+      };
+      expect(queryType?.fields).to.deep.equal([
+        {
+          name: 'someField',
+          ...defaults,
+          type: {
+            kind: 'NON_NULL',
+            name: null,
+            ofType: { kind: 'SCALAR', name: 'String', ofType: null },
+          },
+        },
+        {
+          name: 'someField2',
+          ...defaults,
+          type: { kind: 'SCALAR', name: 'String', ofType: null },
+        },
+        {
+          name: 'someField3',
+          ...defaults,
+          type: { kind: 'SCALAR', name: 'String', ofType: null },
+        },
+      ]);
+    });
+
+    it('returns semantic-non-null types in full mode', () => {
+      const schema = buildSchema(`
+        @SemanticNullability
+        type Query {
+          someField: String!
+          someField2: String
+          someField3: String?
+        }
+      `);
+
+      const source = getIntrospectionQuery({
+        nullability: 'FULL',
+      });
+
+      const result = graphqlSync({ schema, source });
+      // @ts-expect-error
+      const queryType = result.data?.__schema?.types.find(
+        // @ts-expect-error
+        (t) => t.name === 'Query',
+      );
+      const defaults = {
+        args: [],
+        deprecationReason: null,
+        description: null,
+        isDeprecated: false,
+      };
+      expect(queryType?.fields).to.deep.equal([
+        {
+          name: 'someField',
+          ...defaults,
+          type: {
+            kind: 'NON_NULL',
+            name: null,
+            ofType: { kind: 'SCALAR', name: 'String', ofType: null },
+          },
+        },
+        {
+          name: 'someField2',
+          ...defaults,
+          type: {
+            kind: 'SEMANTIC_NON_NULL',
+            name: null,
+            ofType: { kind: 'SCALAR', name: 'String', ofType: null },
+          },
+        },
+        {
+          name: 'someField3',
+          ...defaults,
+          type: { kind: 'SCALAR', name: 'String', ofType: null },
+        },
+      ]);
+    });
+  });
 });
diff --git a/src/type/directives.ts b/src/type/directives.ts
index 6881f20532..276eb38aa7 100644
--- a/src/type/directives.ts
+++ b/src/type/directives.ts
@@ -165,6 +165,17 @@ export const GraphQLSkipDirective: GraphQLDirective = new GraphQLDirective({
   },
 });
 
+/**
+ * Used to indicate that the nullability of the document will be parsed as semantic-non-null types.
+ */
+export const GraphQLSemanticNullabilityDirective: GraphQLDirective =
+  new GraphQLDirective({
+    name: 'SemanticNullability',
+    description:
+      'Indicates that the nullability of the document will be parsed as semantic-non-null types.',
+    locations: [DirectiveLocation.SCHEMA],
+  });
+
 /**
  * Constant string used for default reason for a deprecation.
  */
diff --git a/src/type/introspection.ts b/src/type/introspection.ts
index b77ea37380..950cf8958e 100644
--- a/src/type/introspection.ts
+++ b/src/type/introspection.ts
@@ -206,36 +206,23 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({
   },
 });
 
-// TODO: rename enum and options
 enum TypeNullability {
-  AUTO = 'AUTO',
   TRADITIONAL = 'TRADITIONAL',
-  SEMANTIC = 'SEMANTIC',
   FULL = 'FULL',
 }
 
-// TODO: rename
 export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({
   name: '__TypeNullability',
-  description: 'TODO',
+  description:
+    'This represents the type of nullability we want to return as part of the introspection.',
   values: {
-    AUTO: {
-      value: TypeNullability.AUTO,
-      description:
-        'Determines nullability mode based on errorPropagation mode.',
-    },
     TRADITIONAL: {
       value: TypeNullability.TRADITIONAL,
       description: 'Turn semantic-non-null types into nullable types.',
     },
-    SEMANTIC: {
-      value: TypeNullability.SEMANTIC,
-      description: 'Turn non-null types into semantic-non-null types.',
-    },
     FULL: {
       value: TypeNullability.FULL,
-      description:
-        'Render the true nullability in the schema; be prepared for new types of nullability in future!',
+      description: 'Allow for returning semantic-non-null types.',
     },
   },
 });
@@ -408,22 +395,11 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
         args: {
           nullability: {
             type: new GraphQLNonNull(__TypeNullability),
-            defaultValue: TypeNullability.AUTO,
+            defaultValue: TypeNullability.TRADITIONAL,
           },
         },
-        resolve: (field, { nullability }, _context, info) => {
-          if (nullability === TypeNullability.FULL) {
-            return field.type;
-          }
-
-          const mode =
-            nullability === TypeNullability.AUTO
-              ? info.errorPropagation
-                ? TypeNullability.TRADITIONAL
-                : TypeNullability.SEMANTIC
-              : nullability;
-          return convertOutputTypeToNullabilityMode(field.type, mode);
-        },
+        resolve: (field, { nullability }, _context) =>
+          convertOutputTypeToNullabilityMode(field.type, nullability),
       },
       isDeprecated: {
         type: new GraphQLNonNull(GraphQLBoolean),
@@ -436,10 +412,9 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
     } as GraphQLFieldConfigMap<GraphQLField<unknown, unknown>, unknown>),
 });
 
-// TODO: move this elsewhere, rename, memoize
 function convertOutputTypeToNullabilityMode(
   type: GraphQLType,
-  mode: TypeNullability.TRADITIONAL | TypeNullability.SEMANTIC,
+  mode: TypeNullability,
 ): GraphQLType {
   if (mode === TypeNullability.TRADITIONAL) {
     if (isNonNullType(type)) {
@@ -455,7 +430,12 @@ function convertOutputTypeToNullabilityMode(
     }
     return type;
   }
-  if (isNonNullType(type) || isSemanticNonNullType(type)) {
+
+  if (isNonNullType(type)) {
+    return new GraphQLNonNull(
+      convertOutputTypeToNullabilityMode(type.ofType, mode),
+    );
+  } else if (isSemanticNonNullType(type)) {
     return new GraphQLSemanticNonNull(
       convertOutputTypeToNullabilityMode(type.ofType, mode),
     );
@@ -464,6 +444,7 @@ function convertOutputTypeToNullabilityMode(
       convertOutputTypeToNullabilityMode(type.ofType, mode),
     );
   }
+
   return type;
 }
 
diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts
index 5c04458c51..48f50d21b7 100644
--- a/src/utilities/__tests__/TypeInfo-test.ts
+++ b/src/utilities/__tests__/TypeInfo-test.ts
@@ -457,4 +457,66 @@ describe('visitWithTypeInfo', () => {
       ['leave', 'SelectionSet', null, 'Human', 'Human'],
     ]);
   });
+
+  it('supports traversals of semantic non-null types', () => {
+    const schema = buildSchema(`
+      @SemanticNullability
+      type Query {
+        id: String!
+        name: String
+        something: String?
+      }
+    `);
+
+    const typeInfo = new TypeInfo(schema);
+
+    const visited: Array<any> = [];
+    const ast = parse('{ id name something }');
+
+    visit(
+      ast,
+      visitWithTypeInfo(typeInfo, {
+        enter(node) {
+          const type = typeInfo.getType();
+          visited.push([
+            'enter',
+            node.kind,
+            node.kind === 'Name' ? node.value : null,
+            String(type),
+          ]);
+        },
+        leave(node) {
+          const type = typeInfo.getType();
+          visited.push([
+            'leave',
+            node.kind,
+            node.kind === 'Name' ? node.value : null,
+            // TODO: inspect currently returns "String" for a nullable type
+            String(type),
+          ]);
+        },
+      }),
+    );
+
+    expect(visited).to.deep.equal([
+      ['enter', 'Document', null, 'undefined'],
+      ['enter', 'OperationDefinition', null, 'Query'],
+      ['enter', 'SelectionSet', null, 'Query'],
+      ['enter', 'Field', null, 'String!'],
+      ['enter', 'Name', 'id', 'String!'],
+      ['leave', 'Name', 'id', 'String!'],
+      ['leave', 'Field', null, 'String!'],
+      ['enter', 'Field', null, 'String'],
+      ['enter', 'Name', 'name', 'String'],
+      ['leave', 'Name', 'name', 'String'],
+      ['leave', 'Field', null, 'String'],
+      ['enter', 'Field', null, 'String'],
+      ['enter', 'Name', 'something', 'String'],
+      ['leave', 'Name', 'something', 'String'],
+      ['leave', 'Field', null, 'String'],
+      ['leave', 'SelectionSet', null, 'Query'],
+      ['leave', 'OperationDefinition', null, 'Query'],
+      ['leave', 'Document', null, 'undefined'],
+    ]);
+  });
 });
diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts
index e8cf046921..59b78024e6 100644
--- a/src/utilities/__tests__/buildClientSchema-test.ts
+++ b/src/utilities/__tests__/buildClientSchema-test.ts
@@ -9,6 +9,7 @@ import {
   assertEnumType,
   GraphQLEnumType,
   GraphQLObjectType,
+  GraphQLSemanticNonNull,
 } from '../../type/definition';
 import {
   GraphQLBoolean,
@@ -983,4 +984,63 @@ describe('Type System: build schema from introspection', () => {
       );
     });
   });
+
+  describe('SemanticNullability', () => {
+    it('should build a client schema with semantic-non-null types', () => {
+      const sdl = dedent`
+        @SemanticNullability
+
+        type Query {
+          foo: String
+          bar: String?
+        }
+      `;
+      const schema = buildSchema(sdl, { assumeValid: true });
+      const introspection = introspectionFromSchema(schema, {
+        nullability: 'FULL',
+      });
+
+      const clientSchema = buildClientSchema(introspection);
+      expect(printSchema(clientSchema)).to.equal(sdl);
+
+      const defaults = {
+        args: [],
+        astNode: undefined,
+        deprecationReason: null,
+        description: null,
+        extensions: {},
+        resolve: undefined,
+        subscribe: undefined,
+      };
+      expect(clientSchema.getType('Query')).to.deep.include({
+        name: 'Query',
+        _fields: {
+          foo: {
+            ...defaults,
+            name: 'foo',
+            type: new GraphQLSemanticNonNull(GraphQLString),
+          },
+          bar: { ...defaults, name: 'bar', type: GraphQLString },
+        },
+      });
+    });
+
+    it('should throw when semantic-non-null types are too deep', () => {
+      const sdl = dedent`
+        @SemanticNullability
+
+        type Query {
+          bar: [[[[[[String?]]]]]]?
+        }
+      `;
+      const schema = buildSchema(sdl, { assumeValid: true });
+      const introspection = introspectionFromSchema(schema, {
+        nullability: 'FULL',
+      });
+
+      expect(() => buildClientSchema(introspection)).to.throw(
+        'Decorated type deeper than introspection query.',
+      );
+    });
+  });
 });
diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts
index 171b0e6b62..a70ff2fb47 100644
--- a/src/utilities/__tests__/extendSchema-test.ts
+++ b/src/utilities/__tests__/extendSchema-test.ts
@@ -50,13 +50,16 @@ function expectASTNode(obj: Maybe<{ readonly astNode: Maybe<ASTNode> }>) {
 function expectSchemaChanges(
   schema: GraphQLSchema,
   extendedSchema: GraphQLSchema,
+  semanticNullability: boolean = false,
 ) {
   const schemaDefinitions = parse(printSchema(schema)).definitions.map((node) =>
-    print(node),
+    print(node, { useSemanticNullability: semanticNullability }),
   );
   return expect(
     parse(printSchema(extendedSchema))
-      .definitions.map((node) => print(node))
+      .definitions.map((node) =>
+        print(node, { useSemanticNullability: semanticNullability }),
+      )
       .filter((def) => !schemaDefinitions.includes(def))
       .join('\n\n'),
   );
@@ -88,6 +91,34 @@ describe('extendSchema', () => {
     });
   });
 
+  it('extends objects by adding new fields in semantic nullability mode', () => {
+    const schema = buildSchema(`
+      @SemanticNullability
+      type Query {
+        someObject: String
+      }
+    `);
+    const extensionSDL = dedent`
+      @SemanticNullability
+      extend type Query {
+        newSemanticNonNullField: String
+        newSemanticNullableField: String?
+        newNonNullField: String!
+      }
+    `;
+    const extendedSchema = extendSchema(schema, parse(extensionSDL));
+
+    expect(validateSchema(extendedSchema)).to.deep.equal([]);
+    expectSchemaChanges(schema, extendedSchema, true).to.equal(dedent`
+      type Query {
+        someObject: String
+        newSemanticNonNullField: String
+        newSemanticNullableField: String?
+        newNonNullField: String!
+      }
+    `);
+  });
+
   it('extends objects by adding new fields', () => {
     const schema = buildSchema(`
       type Query {
@@ -99,6 +130,7 @@ describe('extendSchema', () => {
         tree: [SomeObject]!
         """Old field description."""
         oldField: String
+
       }
 
       interface SomeInterface {
diff --git a/src/utilities/__tests__/findBreakingChanges-test.ts b/src/utilities/__tests__/findBreakingChanges-test.ts
index ba526deb48..f54b8c08ed 100644
--- a/src/utilities/__tests__/findBreakingChanges-test.ts
+++ b/src/utilities/__tests__/findBreakingChanges-test.ts
@@ -577,6 +577,106 @@ describe('findBreakingChanges', () => {
     expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([]);
   });
 
+  it('should consider semantic non-null output types that change type as breaking', () => {
+    const oldSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String
+      }
+    `);
+
+    const newSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: Int
+      }
+    `);
+
+    expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([
+      {
+        description: 'Type1.field1 changed type from String to Int.',
+        type: BreakingChangeType.FIELD_CHANGED_KIND,
+      },
+    ]);
+  });
+
+  it('should consider output types that move away from SemanticNonNull to non-null as non-breaking', () => {
+    const oldSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String
+      }
+    `);
+
+    const newSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String!
+      }
+    `);
+
+    expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([]);
+  });
+
+  it('should consider output types that move away from nullable to semantic non-null as non-breaking', () => {
+    const oldSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String?
+      }
+    `);
+
+    const newSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String
+      }
+    `);
+
+    expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([]);
+  });
+
+  it('should consider list output types that move away from nullable to semantic non-null as non-breaking', () => {
+    const oldSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: [String?]?
+      }
+    `);
+
+    const newSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: [String]
+      }
+    `);
+
+    expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([]);
+  });
+
+  it('should consider output types that move away from SemanticNonNull to null as breaking', () => {
+    const oldSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String
+      }
+    `);
+
+    const newSchema = buildSchema(`
+      @SemanticNullability
+      type Type1 {
+        field1: String?
+      }
+    `);
+
+    expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([
+      {
+        description: 'Type1.field1 changed type from String to String.',
+        type: BreakingChangeType.FIELD_CHANGED_KIND,
+      },
+    ]);
+  });
+
   it('should detect interfaces removed from types', () => {
     const oldSchema = buildSchema(`
       interface Interface1
diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.ts b/src/utilities/__tests__/getIntrospectionQuery-test.ts
index 86d1c549db..6aa31ae971 100644
--- a/src/utilities/__tests__/getIntrospectionQuery-test.ts
+++ b/src/utilities/__tests__/getIntrospectionQuery-test.ts
@@ -125,6 +125,15 @@ describe('getIntrospectionQuery', () => {
     expectIntrospectionQuery({ oneOf: false }).toNotMatch('isOneOf');
   });
 
+  it('include "nullability" argument on object fields', () => {
+    expect(
+      getIntrospectionQuery({ nullability: 'TRADITIONAL' }),
+    ).to.not.contain('type(nullability:');
+    expect(getIntrospectionQuery({ nullability: 'FULL' })).to.contain(
+      'type(nullability:',
+    );
+  });
+
   it('include deprecated input field and args', () => {
     expectIntrospectionQuery().toMatch('includeDeprecated: true', 2);
 
diff --git a/src/utilities/__tests__/lexicographicSortSchema-test.ts b/src/utilities/__tests__/lexicographicSortSchema-test.ts
index bce12e3ac5..2187964740 100644
--- a/src/utilities/__tests__/lexicographicSortSchema-test.ts
+++ b/src/utilities/__tests__/lexicographicSortSchema-test.ts
@@ -63,6 +63,60 @@ describe('lexicographicSortSchema', () => {
     `);
   });
 
+  it('sort fields w/ semanticNonNull', () => {
+    const sorted = sortSDL(`
+      @SemanticNullability
+
+      input Bar {
+        barB: String!
+        barA: String
+        barC: [String]
+      }
+
+      interface FooInterface {
+        fooB: String!
+        fooA: String
+        fooC: [String]
+      }
+
+      type FooType implements FooInterface {
+        fooC: [String]
+        fooA: String
+        fooB: String!
+      }
+
+      type Query {
+        dummy(arg: Bar): FooType?
+      }
+    `);
+
+    expect(sorted).to.equal(dedent`
+      @SemanticNullability
+
+      input Bar {
+        barA: String
+        barB: String!
+        barC: [String]
+      }
+
+      interface FooInterface {
+        fooA: String
+        fooB: String!
+        fooC: [String]
+      }
+
+      type FooType implements FooInterface {
+        fooA: String
+        fooB: String!
+        fooC: [String]
+      }
+
+      type Query {
+        dummy(arg: Bar): FooType?
+      }
+    `);
+  });
+
   it('sort implemented interfaces', () => {
     const sorted = sortSDL(`
       interface FooA {
diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts
index b651bf16a8..e94bd2fb79 100644
--- a/src/utilities/__tests__/printSchema-test.ts
+++ b/src/utilities/__tests__/printSchema-test.ts
@@ -782,7 +782,7 @@ describe('Type System Printer', () => {
         name: String!
         description: String
         args(includeDeprecated: Boolean = false): [__InputValue!]!
-        type(nullability: __TypeNullability! = AUTO): __Type!
+        type(nullability: __TypeNullability! = TRADITIONAL): __Type!
         isDeprecated: Boolean!
         deprecationReason: String
       }
@@ -803,20 +803,14 @@ describe('Type System Printer', () => {
         deprecationReason: String
       }
 
-      """TODO"""
+      """
+      This represents the type of nullability we want to return as part of the introspection.
+      """
       enum __TypeNullability {
-        """Determines nullability mode based on errorPropagation mode."""
-        AUTO
-      
         """Turn semantic-non-null types into nullable types."""
         TRADITIONAL
       
-        """Turn non-null types into semantic-non-null types."""
-        SEMANTIC
-      
-        """
-        Render the true nullability in the schema; be prepared for new types of nullability in future!
-        """
+        """Allow for returning semantic-non-null types."""
         FULL
       }
 
diff --git a/src/utilities/__tests__/typeComparators-test.ts b/src/utilities/__tests__/typeComparators-test.ts
index f2709bf740..f7dbe6905f 100644
--- a/src/utilities/__tests__/typeComparators-test.ts
+++ b/src/utilities/__tests__/typeComparators-test.ts
@@ -7,6 +7,7 @@ import {
   GraphQLList,
   GraphQLNonNull,
   GraphQLObjectType,
+  GraphQLSemanticNonNull,
   GraphQLUnionType,
 } from '../../type/definition';
 import { GraphQLFloat, GraphQLInt, GraphQLString } from '../../type/scalars';
@@ -20,6 +21,15 @@ describe('typeComparators', () => {
       expect(isEqualType(GraphQLString, GraphQLString)).to.equal(true);
     });
 
+    it('semantic non-null is equal to semantic non-null', () => {
+      expect(
+        isEqualType(
+          new GraphQLSemanticNonNull(GraphQLString),
+          new GraphQLSemanticNonNull(GraphQLString),
+        ),
+      ).to.equal(true);
+    });
+
     it('int and float are not equal', () => {
       expect(isEqualType(GraphQLInt, GraphQLFloat)).to.equal(false);
     });
@@ -81,6 +91,50 @@ describe('typeComparators', () => {
       ).to.equal(true);
     });
 
+    it('semantic non-null is subtype of nullable', () => {
+      const schema = testSchema({ field: { type: GraphQLString } });
+      expect(
+        isTypeSubTypeOf(
+          schema,
+          new GraphQLSemanticNonNull(GraphQLInt),
+          GraphQLInt,
+        ),
+      ).to.equal(true);
+    });
+
+    it('semantic non-null is subtype of semantic non-null', () => {
+      const schema = testSchema({ field: { type: GraphQLString } });
+      expect(
+        isTypeSubTypeOf(
+          schema,
+          new GraphQLSemanticNonNull(GraphQLInt),
+          new GraphQLSemanticNonNull(GraphQLInt),
+        ),
+      ).to.equal(true);
+    });
+
+    it('semantic non-null is a subtype of non-null', () => {
+      const schema = testSchema({ field: { type: GraphQLString } });
+      expect(
+        isTypeSubTypeOf(
+          schema,
+          new GraphQLSemanticNonNull(GraphQLInt),
+          new GraphQLNonNull(GraphQLInt),
+        ),
+      ).to.equal(true);
+    });
+
+    it('non-null is a subtype of semantic non-null', () => {
+      const schema = testSchema({ field: { type: GraphQLString } });
+      expect(
+        isTypeSubTypeOf(
+          schema,
+          new GraphQLNonNull(GraphQLInt),
+          new GraphQLSemanticNonNull(GraphQLInt),
+        ),
+      ).to.equal(true);
+    });
+
     it('nullable is not subtype of non-null', () => {
       const schema = testSchema({ field: { type: GraphQLString } });
       expect(
diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts
index 9b0809adf5..739e758bf4 100644
--- a/src/utilities/buildClientSchema.ts
+++ b/src/utilities/buildClientSchema.ts
@@ -138,6 +138,7 @@ export function buildClientSchema(
       const nullableType = getType(nullableRef);
       return new GraphQLNonNull(assertNullableType(nullableType));
     }
+
     if (typeRef.kind === TypeKind.SEMANTIC_NON_NULL) {
       const nullableRef = typeRef.ofType;
       if (!nullableRef) {
diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts
index dda0e7f19a..cf5dc40797 100644
--- a/src/utilities/getIntrospectionQuery.ts
+++ b/src/utilities/getIntrospectionQuery.ts
@@ -42,13 +42,11 @@ export interface IntrospectionOptions {
   /**
    * Choose the type of nullability you would like to see.
    *
-   * - AUTO: SEMANTIC if errorPropagation is set to false, otherwise TRADITIONAL
    * - TRADITIONAL: all GraphQLSemanticNonNull will be unwrapped
-   * - SEMANTIC: all GraphQLNonNull will be converted to GraphQLSemanticNonNull
    * - FULL: the true nullability will be returned
    *
    */
-  nullability?: 'AUTO' | 'TRADITIONAL' | 'SEMANTIC' | 'FULL';
+  nullability?: 'TRADITIONAL' | 'FULL';
 }
 
 /**
@@ -63,7 +61,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
     schemaDescription: false,
     inputValueDeprecation: false,
     oneOf: false,
-    nullability: null,
+    nullability: 'TRADITIONAL',
     ...options,
   };
 
@@ -118,7 +116,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
         args${inputDeprecation('(includeDeprecated: true)')} {
           ...InputValue
         }
-        type${nullability ? `(nullability: ${nullability})` : ''} {
+        type${nullability === 'FULL' ? `(nullability: ${nullability})` : ''} {
           ...TypeRef
         }
         isDeprecated
diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts
index edac6262c5..e44c280e20 100644
--- a/src/utilities/printSchema.ts
+++ b/src/utilities/printSchema.ts
@@ -18,9 +18,11 @@ import type {
   GraphQLUnionType,
 } from '../type/definition';
 import {
+  GraphQLSemanticNonNull,
   isEnumType,
   isInputObjectType,
   isInterfaceType,
+  isNullableType,
   isObjectType,
   isScalarType,
   isUnionType,
@@ -59,11 +61,19 @@ function printFilteredSchema(
 ): string {
   const directives = schema.getDirectives().filter(directiveFilter);
   const types = Object.values(schema.getTypeMap()).filter(typeFilter);
+  const hasSemanticNonNull = types.some(
+    (type) =>
+      (isObjectType(type) || isInterfaceType(type)) &&
+      Object.values(type.getFields()).some(
+        (field) => field.type instanceof GraphQLSemanticNonNull,
+      ),
+  );
 
   return [
+    hasSemanticNonNull ? '@SemanticNullability' : '',
     printSchemaDefinition(schema),
     ...directives.map((directive) => printDirective(directive)),
-    ...types.map((type) => printType(type)),
+    ...types.map((type) => printType(type, hasSemanticNonNull)),
   ]
     .filter(Boolean)
     .join('\n\n');
@@ -128,15 +138,18 @@ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean {
   return true;
 }
 
-export function printType(type: GraphQLNamedType): string {
+export function printType(
+  type: GraphQLNamedType,
+  hasSemanticNonNull: boolean = false,
+): string {
   if (isScalarType(type)) {
     return printScalar(type);
   }
   if (isObjectType(type)) {
-    return printObject(type);
+    return printObject(type, hasSemanticNonNull);
   }
   if (isInterfaceType(type)) {
-    return printInterface(type);
+    return printInterface(type, hasSemanticNonNull);
   }
   if (isUnionType(type)) {
     return printUnion(type);
@@ -167,21 +180,27 @@ function printImplementedInterfaces(
     : '';
 }
 
-function printObject(type: GraphQLObjectType): string {
+function printObject(
+  type: GraphQLObjectType,
+  hasSemanticNonNull: boolean,
+): string {
   return (
     printDescription(type) +
     `type ${type.name}` +
     printImplementedInterfaces(type) +
-    printFields(type)
+    printFields(type, hasSemanticNonNull)
   );
 }
 
-function printInterface(type: GraphQLInterfaceType): string {
+function printInterface(
+  type: GraphQLInterfaceType,
+  hasSemanticNonNull: boolean,
+): string {
   return (
     printDescription(type) +
     `interface ${type.name}` +
     printImplementedInterfaces(type) +
-    printFields(type)
+    printFields(type, hasSemanticNonNull)
   );
 }
 
@@ -217,7 +236,10 @@ function printInputObject(type: GraphQLInputObjectType): string {
   );
 }
 
-function printFields(type: GraphQLObjectType | GraphQLInterfaceType): string {
+function printFields(
+  type: GraphQLObjectType | GraphQLInterfaceType,
+  hasSemanticNonNull: boolean,
+): string {
   const fields = Object.values(type.getFields()).map(
     (f, i) =>
       printDescription(f, '  ', !i) +
@@ -225,7 +247,9 @@ function printFields(type: GraphQLObjectType | GraphQLInterfaceType): string {
       f.name +
       printArgs(f.args, '  ') +
       ': ' +
-      String(f.type) +
+      (hasSemanticNonNull && isNullableType(f.type)
+        ? `${f.type}?`
+        : String(f.type)) +
       printDeprecated(f.deprecationReason),
   );
   return printBlock(fields);
diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts
index 13311780ff..5b7c498c65 100644
--- a/src/utilities/typeComparators.ts
+++ b/src/utilities/typeComparators.ts
@@ -53,7 +53,7 @@ export function isTypeSubTypeOf(
 
   // If superType is non-null, maybeSubType must also be non-null.
   if (isNonNullType(superType)) {
-    if (isNonNullType(maybeSubType)) {
+    if (isNonNullType(maybeSubType) || isSemanticNonNullType(maybeSubType)) {
       return isTypeSubTypeOf(schema, maybeSubType.ofType, superType.ofType);
     }
     return false;
diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts
index c5d5f537a2..c89b66ea96 100644
--- a/src/utilities/typeFromAST.ts
+++ b/src/utilities/typeFromAST.ts
@@ -50,10 +50,12 @@ export function typeFromAST(
       const innerType = typeFromAST(schema, typeNode.type);
       return innerType && new GraphQLNonNull(innerType);
     }
-    case Kind.SEMANTIC_NON_NULL_TYPE: {
-      const innerType = typeFromAST(schema, typeNode.type);
-      return innerType && new GraphQLSemanticNonNull(innerType);
-    }
+    // We only use typeFromAST for fragment/variable type inference
+    // which should not be affected by semantic non-null types
+    // case Kind.SEMANTIC_NON_NULL_TYPE: {
+    //   const innerType = typeFromAST(schema, typeNode.type);
+    //   return innerType && new GraphQLSemanticNonNull(innerType);
+    // }
     case Kind.NAMED_TYPE:
       return schema.getType(typeNode.name.value);
   }
diff --git a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
index 7418c3e4e8..a9d7ef2d14 100644
--- a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
+++ b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
@@ -1192,4 +1192,149 @@ describe('Validate: Overlapping fields can be merged', () => {
       }
     `);
   });
+
+  describe('semantic non-null', () => {
+    const schema = buildSchema(`
+      @SemanticNullability
+      type Query {
+        box: Box
+      }
+
+      interface Box {
+        id: String
+      }
+
+      type IntBox implements Box {
+        id: String
+        field: Int
+        field2: Int?
+        field3: Int
+      }
+
+      type StringBox implements Box {
+        id: String
+        field: String
+        field2: Int
+        field3: Int
+      }
+    `);
+
+    it('does not error when non-null and semantic non-null overlap with same type', () => {
+      expectErrorsWithSchema(
+        schema,
+        `
+          {
+            box {
+              ... on IntBox {
+                id
+              }
+              ... on StringBox {
+                id
+              }
+            }
+          }
+        `,
+      ).toDeepEqual([]);
+    });
+
+    it('does not error when two semantic non-null fields overlap with same type', () => {
+      expectErrorsWithSchema(
+        schema,
+        `
+          {
+            box {
+              ... on IntBox {
+                field3
+              }
+              ... on StringBox {
+                field3
+              }
+            }
+          }
+        `,
+      ).toDeepEqual([]);
+    });
+
+    it('errors when 2 semantic non-null fields overlap with different types', () => {
+      expectErrorsWithSchema(
+        schema,
+        `
+          {
+            box {
+              ... on IntBox {
+                field
+              }
+              ... on StringBox {
+                field
+              }
+            }
+          }
+        `,
+      ).toDeepEqual([
+        {
+          message:
+            'Fields "field" conflict because they return conflicting types "Int" and "String". Use different aliases on the fields to fetch both if this was intentional.',
+          locations: [
+            { line: 5, column: 17 },
+            { line: 8, column: 17 },
+          ],
+        },
+      ]);
+    });
+
+    it('errors when semantic non-null and nullable fields overlap with different types', () => {
+      expectErrorsWithSchema(
+        schema,
+        `
+          {
+            box {
+               ... on StringBox {
+                field2
+              }
+              ... on IntBox {
+                field2
+              }
+            }
+          }
+        `,
+      ).toDeepEqual([
+        {
+          message:
+            'Fields "field2" conflict because they return conflicting types "Int" and "Int". Use different aliases on the fields to fetch both if this was intentional.',
+          locations: [
+            { line: 5, column: 17 },
+            { line: 8, column: 17 },
+          ],
+        },
+      ]);
+    });
+
+    it('errors when non-null and semantic non-null overlap with different types', () => {
+      expectErrorsWithSchema(
+        schema,
+        `
+          {
+            box {
+              ... on IntBox {
+                field2
+              }
+              ... on StringBox {
+                field2
+              }
+            }
+          }
+        `,
+      ).toDeepEqual([
+        {
+          // TODO: inspect currently returns "Int" for both types
+          message:
+            'Fields "field2" conflict because they return conflicting types "Int" and "Int". Use different aliases on the fields to fetch both if this was intentional.',
+          locations: [
+            { line: 5, column: 17 },
+            { line: 8, column: 17 },
+          ],
+        },
+      ]);
+    });
+  });
 });

From 4c8a02bde8556b3f06a7b63fb20059833d7f3036 Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Wed, 12 Feb 2025 05:30:14 +0100
Subject: [PATCH 07/10] Remove SemanticNonNull from TypeNode

This type is reused for variables, inputs and list-types which
can't be smantically non-null. list-types can be but only if they
are used in an SDL context.

This is kind of a short-coming of our types, we conflate SDL
and execution language.
---
 .../__tests__/semantic-nullability-test.ts    | 31 +++++--------------
 src/language/ast.ts                           | 10 ++----
 src/language/parser.ts                        | 11 ++++---
 src/utilities/extendSchema.ts                 |  5 ++-
 src/utilities/typeFromAST.ts                  | 14 ++-------
 5 files changed, 22 insertions(+), 49 deletions(-)

diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts
index 613ab91d1c..38c60825ec 100644
--- a/src/execution/__tests__/semantic-nullability-test.ts
+++ b/src/execution/__tests__/semantic-nullability-test.ts
@@ -36,9 +36,7 @@ describe('Execute: Handles Semantic Nullability', () => {
 
   it('SemanticNonNull throws error on null without error', async () => {
     const data = {
-      a: () => 'Apple',
       b: () => null,
-      c: () => 'Cookie',
     };
 
     const document = parse(`
@@ -53,11 +51,8 @@ describe('Execute: Handles Semantic Nullability', () => {
       rootValue: data,
     });
 
-    const executable = document.definitions?.values().next()
-      .value as ExecutableDefinitionNode;
-    const selectionSet = executable.selectionSet.selections
-      .values()
-      .next().value;
+    const executable = document.definitions[0] as ExecutableDefinitionNode;
+    const selectionSet = executable.selectionSet.selections[0];
 
     expect(result).to.deep.equal({
       data: {
@@ -77,11 +72,9 @@ describe('Execute: Handles Semantic Nullability', () => {
 
   it('SemanticNonNull succeeds on null with error', async () => {
     const data = {
-      a: () => 'Apple',
       b: () => {
         throw new Error('Something went wrong');
       },
-      c: () => 'Cookie',
     };
 
     const document = parse(`
@@ -90,11 +83,8 @@ describe('Execute: Handles Semantic Nullability', () => {
         }
       `);
 
-    const executable = document.definitions?.values().next()
-      .value as ExecutableDefinitionNode;
-    const selectionSet = executable.selectionSet.selections
-      .values()
-      .next().value;
+    const executable = document.definitions[0] as ExecutableDefinitionNode;
+    const selectionSet = executable.selectionSet.selections[0];
 
     const result = await execute({
       schema: new GraphQLSchema({ query: DataType }),
@@ -121,9 +111,6 @@ describe('Execute: Handles Semantic Nullability', () => {
     };
 
     const data = {
-      a: () => 'Apple',
-      b: () => null,
-      c: () => 'Cookie',
       d: () => deepData,
     };
 
@@ -141,13 +128,9 @@ describe('Execute: Handles Semantic Nullability', () => {
       rootValue: data,
     });
 
-    const executable = document.definitions?.values().next()
-      .value as ExecutableDefinitionNode;
-    const dSelectionSet = executable.selectionSet.selections.values().next()
-      .value as FieldNode;
-    const fSelectionSet = dSelectionSet.selectionSet?.selections
-      .values()
-      .next().value;
+    const executable = document.definitions[0] as ExecutableDefinitionNode;
+    const dSelectionSet = executable.selectionSet.selections[0] as FieldNode;
+    const fSelectionSet = dSelectionSet.selectionSet?.selections[0];
 
     expect(result).to.deep.equal({
       data: {
diff --git a/src/language/ast.ts b/src/language/ast.ts
index 21c4160464..2149034e07 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -529,11 +529,7 @@ export interface SemanticNonNullTypeNode {
 
 /** Type Reference */
 
-export type TypeNode =
-  | NamedTypeNode
-  | ListTypeNode
-  | NonNullTypeNode
-  | SemanticNonNullTypeNode;
+export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode;
 
 export interface NamedTypeNode {
   readonly kind: Kind.NAMED_TYPE;
@@ -544,7 +540,7 @@ export interface NamedTypeNode {
 export interface ListTypeNode {
   readonly kind: Kind.LIST_TYPE;
   readonly loc?: Location;
-  readonly type: TypeNode;
+  readonly type: TypeNode | SemanticNonNullTypeNode;
 }
 
 export interface NonNullTypeNode {
@@ -609,7 +605,7 @@ export interface FieldDefinitionNode {
   readonly description?: StringValueNode;
   readonly name: NameNode;
   readonly arguments?: ReadonlyArray<InputValueDefinitionNode>;
-  readonly type: TypeNode;
+  readonly type: TypeNode | SemanticNonNullTypeNode;
   readonly directives?: ReadonlyArray<ConstDirectiveNode>;
 }
 
diff --git a/src/language/parser.ts b/src/language/parser.ts
index 790aee3b4b..c96dd25ca6 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -184,7 +184,7 @@ export function parseConstValue(
 export function parseType(
   source: string | Source,
   options?: ParseOptions | undefined,
-): TypeNode {
+): TypeNode | SemanticNonNullTypeNode {
   const parser = new Parser(source, options);
   parser.expectToken(TokenKind.SOF);
   const type = parser.parseTypeReference();
@@ -403,7 +403,8 @@ export class Parser {
     return this.node<VariableDefinitionNode>(this._lexer.token, {
       kind: Kind.VARIABLE_DEFINITION,
       variable: this.parseVariable(),
-      type: (this.expectToken(TokenKind.COLON), this.parseTypeReference()),
+      type: (this.expectToken(TokenKind.COLON),
+      this.parseTypeReference()) as TypeNode,
       defaultValue: this.expectOptionalToken(TokenKind.EQUALS)
         ? this.parseConstValueLiteral()
         : undefined,
@@ -773,7 +774,7 @@ export class Parser {
    *   - ListType
    *   - NonNullType
    */
-  parseTypeReference(): TypeNode {
+  parseTypeReference(): TypeNode | SemanticNonNullTypeNode {
     const start = this._lexer.token;
     let type;
     if (this.expectOptionalToken(TokenKind.BRACKET_L)) {
@@ -781,7 +782,7 @@ export class Parser {
       this.expectToken(TokenKind.BRACKET_R);
       type = this.node<ListTypeNode>(start, {
         kind: Kind.LIST_TYPE,
-        type: innerType,
+        type: innerType as TypeNode,
       });
     } else {
       type = this.parseNamedType();
@@ -992,7 +993,7 @@ export class Parser {
       kind: Kind.INPUT_VALUE_DEFINITION,
       description,
       name,
-      type,
+      type: type as TypeNode,
       defaultValue,
       directives,
     });
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index 876aae277f..440df30c4c 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -24,6 +24,7 @@ import type {
   ScalarTypeExtensionNode,
   SchemaDefinitionNode,
   SchemaExtensionNode,
+  SemanticNonNullTypeNode,
   TypeDefinitionNode,
   TypeNode,
   UnionTypeDefinitionNode,
@@ -431,7 +432,9 @@ export function extendSchemaImpl(
     return type;
   }
 
-  function getWrappedType(node: TypeNode): GraphQLType {
+  function getWrappedType(
+    node: TypeNode | SemanticNonNullTypeNode,
+  ): GraphQLType {
     if (node.kind === Kind.LIST_TYPE) {
       return new GraphQLList(getWrappedType(node.type));
     }
diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts
index c89b66ea96..62e66826b1 100644
--- a/src/utilities/typeFromAST.ts
+++ b/src/utilities/typeFromAST.ts
@@ -7,11 +7,7 @@ import type {
 import { Kind } from '../language/kinds';
 
 import type { GraphQLNamedType, GraphQLType } from '../type/definition';
-import {
-  GraphQLList,
-  GraphQLNonNull,
-  GraphQLSemanticNonNull,
-} from '../type/definition';
+import { GraphQLList, GraphQLNonNull } from '../type/definition';
 import type { GraphQLSchema } from '../type/schema';
 
 /**
@@ -43,19 +39,13 @@ export function typeFromAST(
 ): GraphQLType | undefined {
   switch (typeNode.kind) {
     case Kind.LIST_TYPE: {
-      const innerType = typeFromAST(schema, typeNode.type);
+      const innerType = typeFromAST(schema, typeNode.type as TypeNode);
       return innerType && new GraphQLList(innerType);
     }
     case Kind.NON_NULL_TYPE: {
       const innerType = typeFromAST(schema, typeNode.type);
       return innerType && new GraphQLNonNull(innerType);
     }
-    // We only use typeFromAST for fragment/variable type inference
-    // which should not be affected by semantic non-null types
-    // case Kind.SEMANTIC_NON_NULL_TYPE: {
-    //   const innerType = typeFromAST(schema, typeNode.type);
-    //   return innerType && new GraphQLSemanticNonNull(innerType);
-    // }
     case Kind.NAMED_TYPE:
       return schema.getType(typeNode.name.value);
   }

From ac213fabdaecb5953eb9680b1b3b6ab46ea75afc Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Wed, 12 Feb 2025 05:40:32 +0100
Subject: [PATCH 08/10] Remove unused funcs

---
 src/execution/__tests__/semantic-nullability-test.ts | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/execution/__tests__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts
index 38c60825ec..c35481a509 100644
--- a/src/execution/__tests__/semantic-nullability-test.ts
+++ b/src/execution/__tests__/semantic-nullability-test.ts
@@ -151,8 +151,6 @@ describe('Execute: Handles Semantic Nullability', () => {
   it('SemanticNullable allows non-null values', async () => {
     const data = {
       a: () => 'Apple',
-      b: () => null,
-      c: () => 'Cookie',
     };
 
     const document = parse(`

From 1861f71082c74d774b9b18a2c5791c54191641d1 Mon Sep 17 00:00:00 2001
From: jdecroock <decroockjovi@gmail.com>
Date: Wed, 12 Feb 2025 08:47:29 +0100
Subject: [PATCH 09/10] Be stricter about types

---
 src/language/ast.ts           | 15 +++++++++++++--
 src/utilities/extendSchema.ts |  7 ++-----
 src/utilities/typeFromAST.ts  |  2 +-
 3 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/src/language/ast.ts b/src/language/ast.ts
index 2149034e07..4469a34424 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -530,6 +530,11 @@ export interface SemanticNonNullTypeNode {
 /** Type Reference */
 
 export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode;
+export type SchemaOutputTypeNode =
+  | NamedTypeNode
+  | ListTypeNode
+  | NonNullTypeNode
+  | SemanticNonNullTypeNode;
 
 export interface NamedTypeNode {
   readonly kind: Kind.NAMED_TYPE;
@@ -540,7 +545,13 @@ export interface NamedTypeNode {
 export interface ListTypeNode {
   readonly kind: Kind.LIST_TYPE;
   readonly loc?: Location;
-  readonly type: TypeNode | SemanticNonNullTypeNode;
+  readonly type: TypeNode;
+}
+
+export interface SchemaListTypeNode {
+  readonly kind: Kind.LIST_TYPE;
+  readonly loc?: Location;
+  readonly type: SchemaOutputTypeNode;
 }
 
 export interface NonNullTypeNode {
@@ -605,7 +616,7 @@ export interface FieldDefinitionNode {
   readonly description?: StringValueNode;
   readonly name: NameNode;
   readonly arguments?: ReadonlyArray<InputValueDefinitionNode>;
-  readonly type: TypeNode | SemanticNonNullTypeNode;
+  readonly type: SchemaOutputTypeNode;
   readonly directives?: ReadonlyArray<ConstDirectiveNode>;
 }
 
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index 440df30c4c..1e9b69c55b 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -24,9 +24,8 @@ import type {
   ScalarTypeExtensionNode,
   SchemaDefinitionNode,
   SchemaExtensionNode,
-  SemanticNonNullTypeNode,
+  SchemaOutputTypeNode,
   TypeDefinitionNode,
-  TypeNode,
   UnionTypeDefinitionNode,
   UnionTypeExtensionNode,
 } from '../language/ast';
@@ -432,9 +431,7 @@ export function extendSchemaImpl(
     return type;
   }
 
-  function getWrappedType(
-    node: TypeNode | SemanticNonNullTypeNode,
-  ): GraphQLType {
+  function getWrappedType(node: SchemaOutputTypeNode): GraphQLType {
     if (node.kind === Kind.LIST_TYPE) {
       return new GraphQLList(getWrappedType(node.type));
     }
diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts
index 62e66826b1..7510df1046 100644
--- a/src/utilities/typeFromAST.ts
+++ b/src/utilities/typeFromAST.ts
@@ -39,7 +39,7 @@ export function typeFromAST(
 ): GraphQLType | undefined {
   switch (typeNode.kind) {
     case Kind.LIST_TYPE: {
-      const innerType = typeFromAST(schema, typeNode.type as TypeNode);
+      const innerType = typeFromAST(schema, typeNode.type);
       return innerType && new GraphQLList(innerType);
     }
     case Kind.NON_NULL_TYPE: {

From 855e4d77d2de4a380e4e5dfa949210b2432deb4c Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Sat, 15 Feb 2025 08:49:47 +0100
Subject: [PATCH 10/10] Remove errorPropagation option

---
 src/execution/__tests__/executor-test.ts |  2 --
 src/execution/execute.ts                 | 11 -----------
 src/graphql.ts                           |  8 --------
 src/type/definition.ts                   |  2 --
 4 files changed, 23 deletions(-)

diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts
index a7bc1c8265..c758d3e426 100644
--- a/src/execution/__tests__/executor-test.ts
+++ b/src/execution/__tests__/executor-test.ts
@@ -263,7 +263,6 @@ describe('Execute: Handles basic execution tasks', () => {
       'rootValue',
       'operation',
       'variableValues',
-      'errorPropagation',
     );
 
     const operation = document.definitions[0];
@@ -276,7 +275,6 @@ describe('Execute: Handles basic execution tasks', () => {
       schema,
       rootValue,
       operation,
-      errorPropagation: true,
     });
 
     const field = operation.selectionSet.selections[0];
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 055b778983..cf5183e126 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -116,7 +116,6 @@ export interface ExecutionContext {
   typeResolver: GraphQLTypeResolver<any, any>;
   subscribeFieldResolver: GraphQLFieldResolver<any, any>;
   errors: Array<GraphQLError>;
-  errorPropagation: boolean;
 }
 
 /**
@@ -154,13 +153,6 @@ export interface ExecutionArgs {
   fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
   typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
   subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
-  /**
-   * Set to `false` to disable error propagation. Experimental.
-   * TODO: describe what this does
-   *
-   * @experimental
-   */
-  errorPropagation?: boolean;
 }
 
 /**
@@ -295,7 +287,6 @@ export function buildExecutionContext(
     fieldResolver,
     typeResolver,
     subscribeFieldResolver,
-    errorPropagation,
   } = args;
 
   let operation: OperationDefinitionNode | undefined;
@@ -357,7 +348,6 @@ export function buildExecutionContext(
     typeResolver: typeResolver ?? defaultTypeResolver,
     subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
     errors: [],
-    errorPropagation: errorPropagation ?? true,
   };
 }
 
@@ -596,7 +586,6 @@ export function buildResolveInfo(
     rootValue: exeContext.rootValue,
     operation: exeContext.operation,
     variableValues: exeContext.variableValues,
-    errorPropagation: exeContext.errorPropagation,
   };
 }
 
diff --git a/src/graphql.ts b/src/graphql.ts
index d3f05f991e..bc6fb9bb72 100644
--- a/src/graphql.ts
+++ b/src/graphql.ts
@@ -66,12 +66,6 @@ export interface GraphQLArgs {
   operationName?: Maybe<string>;
   fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
   typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
-  /**
-   * Set to `false` to disable error propagation. Experimental.
-   *
-   * @experimental
-   */
-  errorPropagation?: boolean;
 }
 
 export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
@@ -112,7 +106,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
     operationName,
     fieldResolver,
     typeResolver,
-    errorPropagation,
   } = args;
 
   // Validate Schema
@@ -145,6 +138,5 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
     operationName,
     fieldResolver,
     typeResolver,
-    errorPropagation,
   });
 }
diff --git a/src/type/definition.ts b/src/type/definition.ts
index bcd3862c89..b0c7d0c52f 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -1087,8 +1087,6 @@ export interface GraphQLResolveInfo {
   readonly rootValue: unknown;
   readonly operation: OperationDefinitionNode;
   readonly variableValues: { [variable: string]: unknown };
-  /** @experimental */
-  readonly errorPropagation: boolean;
 }
 
 /**