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__/semantic-nullability-test.ts b/src/execution/__tests__/semantic-nullability-test.ts
new file mode 100644
index 0000000000..c35481a509
--- /dev/null
+++ b/src/execution/__tests__/semantic-nullability-test.ts
@@ -0,0 +1,174 @@
+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,
+} 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: 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 = {
+      b: () => null,
+    };
+
+    const document = parse(`
+        query {
+          b
+        }
+      `);
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    const executable = document.definitions[0] as ExecutableDefinitionNode;
+    const selectionSet = executable.selectionSet.selections[0];
+
+    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 = {
+      b: () => {
+        throw new Error('Something went wrong');
+      },
+    };
+
+    const document = parse(`
+        query {
+          b
+        }
+      `);
+
+    const executable = document.definitions[0] as ExecutableDefinitionNode;
+    const selectionSet = executable.selectionSet.selections[0];
+
+    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 = {
+      d: () => deepData,
+    };
+
+    const document = parse(`
+        query {
+          d {
+            f
+          }
+        }
+      `);
+
+    const result = await execute({
+      schema: new GraphQLSchema({ query: DataType }),
+      document,
+      rootValue: data,
+    });
+
+    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: {
+        d: null,
+      },
+      errors: [
+        new GraphQLError(
+          'Cannot return null for non-nullable field DeepDataType.f.',
+          {
+            nodes: fSelectionSet,
+            path: ['d', 'f'],
+          },
+        ),
+      ],
+    });
+  });
+
+  it('SemanticNullable allows non-null values', async () => {
+    const data = {
+      a: () => 'Apple',
+    };
+
+    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..cf5183e126 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -43,6 +43,7 @@ import {
   isListType,
   isNonNullType,
   isObjectType,
+  isSemanticNonNullType,
 } from '../type/definition';
 import {
   SchemaMetaFieldDef,
@@ -658,6 +659,25 @@ 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 result value is null or undefined then return null.
   if (result == null) {
     return null;
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..f3577ef64d 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -657,4 +657,64 @@ describe('Parser', () => {
       });
     });
   });
+
+  describe('parseDocumentDirective', () => {
+    it("doesn't 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.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..a2e3fa070d 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,41 @@ describe('Printer: SDL document', () => {
       }
     `);
   });
+
+  it('prints NamedType', () => {
+    expect(
+      print(parseType('MyType', { allowSemanticNullability: false }), {
+        useSemanticNullability: false,
+      }),
+    ).to.equal(dedent`MyType`);
+  });
+
+  it('prints SemanticNullableType', () => {
+    expect(
+      print(parseType('MyType?', { allowSemanticNullability: true }), {
+        useSemanticNullability: true,
+      }),
+    ).to.equal(dedent`MyType?`);
+  });
+
+  it('prints SemanticNonNullType', () => {
+    expect(
+      print(parseType('MyType', { allowSemanticNullability: true }), {
+        useSemanticNullability: true,
+      }),
+    ).to.equal(dedent`MyType`);
+  });
+
+  it('prints NonNullType', () => {
+    expect(
+      print(parseType('MyType!', { allowSemanticNullability: true }), {
+        useSemanticNullability: true,
+      }),
+    ).to.equal(dedent`MyType!`);
+    expect(
+      print(parseType('MyType!', { allowSemanticNullability: false }), {
+        useSemanticNullability: true,
+      }),
+    ).to.equal(dedent`MyType!`);
+  });
 });
diff --git a/src/language/ast.ts b/src/language/ast.ts
index 6137eb6c1a..4469a34424 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -161,6 +161,7 @@ export type ASTNode =
   | NamedTypeNode
   | ListTypeNode
   | NonNullTypeNode
+  | SemanticNonNullTypeNode
   | SchemaDefinitionNode
   | OperationTypeDefinitionNode
   | ScalarTypeDefinitionNode
@@ -235,6 +236,7 @@ export const QueryDocumentKeys: {
   NamedType: ['name'],
   ListType: ['type'],
   NonNullType: ['type'],
+  SemanticNonNullType: ['type'],
 
   SchemaDefinition: ['description', 'directives', 'operationTypes'],
   OperationTypeDefinition: ['type'],
@@ -519,9 +521,20 @@ export interface ConstDirectiveNode {
   readonly arguments?: ReadonlyArray<ConstArgumentNode>;
 }
 
+export interface SemanticNonNullTypeNode {
+  readonly kind: Kind.SEMANTIC_NON_NULL_TYPE;
+  readonly loc?: Location;
+  readonly type: NamedTypeNode | ListTypeNode;
+}
+
 /** Type Reference */
 
 export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode;
+export type SchemaOutputTypeNode =
+  | NamedTypeNode
+  | ListTypeNode
+  | NonNullTypeNode
+  | SemanticNonNullTypeNode;
 
 export interface NamedTypeNode {
   readonly kind: Kind.NAMED_TYPE;
@@ -535,6 +548,12 @@ export interface ListTypeNode {
   readonly type: TypeNode;
 }
 
+export interface SchemaListTypeNode {
+  readonly kind: Kind.LIST_TYPE;
+  readonly loc?: Location;
+  readonly type: SchemaOutputTypeNode;
+}
+
 export interface NonNullTypeNode {
   readonly kind: Kind.NON_NULL_TYPE;
   readonly loc?: Location;
@@ -597,7 +616,7 @@ export interface FieldDefinitionNode {
   readonly description?: StringValueNode;
   readonly name: NameNode;
   readonly arguments?: ReadonlyArray<InputValueDefinitionNode>;
-  readonly type: TypeNode;
+  readonly type: SchemaOutputTypeNode;
   readonly directives?: ReadonlyArray<ConstDirectiveNode>;
 }
 
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..e91373746c 100644
--- a/src/language/kinds.ts
+++ b/src/language/kinds.ts
@@ -37,6 +37,7 @@ enum Kind {
   NAMED_TYPE = 'NamedType',
   LIST_TYPE = 'ListType',
   NON_NULL_TYPE = 'NonNullType',
+  SEMANTIC_NON_NULL_TYPE = 'SemanticNonNullType',
 
   /** Type System Definitions */
   SCHEMA_DEFINITION = 'SchemaDefinition',
diff --git a/src/language/lexer.ts b/src/language/lexer.ts
index 818f81b286..86ff5edb6f 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,16 @@ 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..c96dd25ca6 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -50,6 +50,7 @@ import type {
   SchemaExtensionNode,
   SelectionNode,
   SelectionSetNode,
+  SemanticNonNullTypeNode,
   StringValueNode,
   Token,
   TypeNode,
@@ -103,6 +104,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-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 associated with the field.
+   *
+   * @experimental
+   */
+  allowSemanticNullability?: boolean;
 }
 
 /**
@@ -171,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();
@@ -258,6 +271,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();
     }
@@ -380,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,
@@ -750,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)) {
@@ -758,12 +782,28 @@ 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();
     }
 
+    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 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,
@@ -953,7 +993,7 @@ export class Parser {
       kind: Kind.INPUT_VALUE_DEFINITION,
       description,
       name,
-      type,
+      type: type as TypeNode,
       defaultValue,
       directives,
     });
diff --git a/src/language/predicates.ts b/src/language/predicates.ts
index a390f4ee55..3ddf52b94c 100644
--- a/src/language/predicates.ts
+++ b/src/language/predicates.ts
@@ -67,7 +67,8 @@ 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
   );
 }
 
diff --git a/src/language/printer.ts b/src/language/printer.ts
index e95c118d8b..66d591d619 100644
--- a/src/language/printer.ts
+++ b/src/language/printer.ts
@@ -2,308 +2,329 @@ 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';
 
+/**
+ * Configuration options to control parser behavior
+ */
+export interface PrintOptions {
+  useSemanticNullability?: boolean;
+}
+
 /**
  * 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
+
+    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;
+      },
+    },
 
-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;
+    VariableDefinition: {
+      leave: ({ variable, type, defaultValue, directives }) =>
+        variable +
+        ': ' +
+        type +
+        wrap(' = ', defaultValue) +
+        wrap(' ', join(directives, ' ')),
     },
-  },
-
-  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], ' ');
+    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,
-  },
-
-  // 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 + '!' },
-
-  // 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)], ' '),
-  },
-};
+
+    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 }, _, 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/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..09c12abb06 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: { kind: 'OBJECT', 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: 'TRADITIONAL',
+                    },
+                  ],
                   type: {
                     kind: 'NON_NULL',
                     name: null,
@@ -640,6 +659,27 @@ describe('Introspection', () => {
               enumValues: null,
               possibleTypes: null,
             },
+            {
+              kind: 'ENUM',
+              name: '__TypeNullability',
+              specifiedByURL: null,
+              fields: null,
+              inputFields: null,
+              interfaces: null,
+              enumValues: [
+                {
+                  name: 'TRADITIONAL',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
+                {
+                  name: 'FULL',
+                  isDeprecated: false,
+                  deprecationReason: null,
+                },
+              ],
+              possibleTypes: null,
+            },
             {
               kind: 'OBJECT',
               name: '__EnumValue',
@@ -1754,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/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts
index 81e721e7df..1c576e8eaa 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();
     });
   });
 
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..b0c7d0c52f 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -66,6 +66,15 @@ export type GraphQLType =
       | GraphQLEnumType
       | GraphQLInputObjectType
       | GraphQLList<GraphQLType>
+    >
+  | GraphQLSemanticNonNull<
+      | GraphQLScalarType
+      | GraphQLObjectType
+      | GraphQLInterfaceType
+      | GraphQLUnionType
+      | GraphQLEnumType
+      | GraphQLInputObjectType
+      | GraphQLList<GraphQLType>
     >;
 
 export function isType(type: unknown): type is GraphQLType {
@@ -77,7 +86,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 +214,32 @@ 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;
+}
+
 /**
  * These types may be used as input types for arguments and directives.
  */
@@ -223,7 +260,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,6 +290,14 @@ export type GraphQLOutputType =
       | GraphQLUnionType
       | GraphQLEnumType
       | GraphQLList<GraphQLOutputType>
+    >
+  | GraphQLSemanticNonNull<
+      | GraphQLScalarType
+      | GraphQLObjectType
+      | GraphQLInterfaceType
+      | GraphQLUnionType
+      | GraphQLEnumType
+      | GraphQLList<GraphQLOutputType>
     >;
 
 export function isOutputType(type: unknown): type is GraphQLOutputType {
@@ -414,16 +461,66 @@ 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();
+  }
+}
+
 /**
  * These types wrap and modify other types
  */
 
 export type GraphQLWrappingType =
   | GraphQLList<GraphQLType>
-  | GraphQLNonNull<GraphQLType>;
+  | GraphQLNonNull<GraphQLType>
+  | GraphQLSemanticNonNull<GraphQLType>;
 
 export function isWrappingType(type: unknown): type is GraphQLWrappingType {
-  return isListType(type) || isNonNullType(type);
+  return isListType(type) || isNonNullType(type) || isSemanticNonNullType(type);
 }
 
 export function assertWrappingType(type: unknown): GraphQLWrappingType {
@@ -446,7 +543,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 +555,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 +564,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;
 
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/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..950cf8958e 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,27 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({
   },
 });
 
+enum TypeNullability {
+  TRADITIONAL = 'TRADITIONAL',
+  FULL = 'FULL',
+}
+
+export const __TypeNullability: GraphQLEnumType = new GraphQLEnumType({
+  name: '__TypeNullability',
+  description:
+    'This represents the type of nullability we want to return as part of the introspection.',
+  values: {
+    TRADITIONAL: {
+      value: TypeNullability.TRADITIONAL,
+      description: 'Turn semantic-non-null types into nullable types.',
+    },
+    FULL: {
+      value: TypeNullability.FULL,
+      description: 'Allow for returning semantic-non-null types.',
+    },
+  },
+});
+
 export const __Type: GraphQLObjectType = new GraphQLObjectType({
   name: '__Type',
   description:
@@ -237,6 +260,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 +392,14 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
       },
       type: {
         type: new GraphQLNonNull(__Type),
-        resolve: (field) => field.type,
+        args: {
+          nullability: {
+            type: new GraphQLNonNull(__TypeNullability),
+            defaultValue: TypeNullability.TRADITIONAL,
+          },
+        },
+        resolve: (field, { nullability }, _context) =>
+          convertOutputTypeToNullabilityMode(field.type, nullability),
       },
       isDeprecated: {
         type: new GraphQLNonNull(GraphQLBoolean),
@@ -379,6 +412,42 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({
     } as GraphQLFieldConfigMap<GraphQLField<unknown, unknown>, unknown>),
 });
 
+function convertOutputTypeToNullabilityMode(
+  type: GraphQLType,
+  mode: TypeNullability,
+): 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)) {
+    return new GraphQLNonNull(
+      convertOutputTypeToNullabilityMode(type.ofType, mode),
+    );
+  } else if (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 +521,7 @@ enum TypeKind {
   INPUT_OBJECT = 'INPUT_OBJECT',
   LIST = 'LIST',
   NON_NULL = 'NON_NULL',
+  SEMANTIC_NON_NULL = 'SEMANTIC_NON_NULL',
 }
 export { TypeKind };
 
@@ -497,6 +567,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 +628,7 @@ export const introspectionTypes: ReadonlyArray<GraphQLNamedType> =
     __Schema,
     __Directive,
     __DirectiveLocation,
+    __TypeNullability,
     __Type,
     __Field,
     __InputValue,
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__/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__/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 86baf0e699..a70ff2fb47 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> }>) {
@@ -50,11 +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(print);
+  const schemaDefinitions = parse(printSchema(schema)).definitions.map((node) =>
+    print(node, { useSemanticNullability: semanticNullability }),
+  );
   return expect(
     parse(printSchema(extendedSchema))
-      .definitions.map(print)
+      .definitions.map((node) =>
+        print(node, { useSemanticNullability: semanticNullability }),
+      )
       .filter((def) => !schemaDefinitions.includes(def))
       .join('\n\n'),
   );
@@ -86,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 {
@@ -97,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 37af4a60f7..e94bd2fb79 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! = TRADITIONAL): __Type!
         isDeprecated: Boolean!
         deprecationReason: String
       }
@@ -800,6 +803,17 @@ describe('Type System Printer', () => {
         deprecationReason: String
       }
 
+      """
+      This represents the type of nullability we want to return as part of the introspection.
+      """
+      enum __TypeNullability {
+        """Turn semantic-non-null types into nullable types."""
+        TRADITIONAL
+      
+        """Allow for returning semantic-non-null types."""
+        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/__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`
         {
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 83f6abada8..739e758bf4 100644
--- a/src/utilities/buildClientSchema.ts
+++ b/src/utilities/buildClientSchema.ts
@@ -22,6 +22,7 @@ import {
   GraphQLNonNull,
   GraphQLObjectType,
   GraphQLScalarType,
+  GraphQLSemanticNonNull,
   GraphQLUnionType,
   isInputType,
   isOutputType,
@@ -137,6 +138,15 @@ 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..1e9b69c55b 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -24,8 +24,8 @@ import type {
   ScalarTypeExtensionNode,
   SchemaDefinitionNode,
   SchemaExtensionNode,
+  SchemaOutputTypeNode,
   TypeDefinitionNode,
-  TypeNode,
   UnionTypeDefinitionNode,
   UnionTypeExtensionNode,
 } from '../language/ast';
@@ -53,6 +53,7 @@ import {
   GraphQLNonNull,
   GraphQLObjectType,
   GraphQLScalarType,
+  GraphQLSemanticNonNull,
   GraphQLUnionType,
   isEnumType,
   isInputObjectType,
@@ -61,6 +62,7 @@ import {
   isNonNullType,
   isObjectType,
   isScalarType,
+  isSemanticNonNullType,
   isUnionType,
 } from '../type/definition';
 import {
@@ -225,6 +227,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);
   }
@@ -425,13 +431,16 @@ export function extendSchemaImpl(
     return type;
   }
 
-  function getWrappedType(node: TypeNode): GraphQLType {
+  function getWrappedType(node: SchemaOutputTypeNode): GraphQLType {
     if (node.kind === Kind.LIST_TYPE) {
       return new GraphQLList(getWrappedType(node.type));
     }
     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));
+    }
     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..cf5dc40797 100644
--- a/src/utilities/getIntrospectionQuery.ts
+++ b/src/utilities/getIntrospectionQuery.ts
@@ -38,6 +38,15 @@ export interface IntrospectionOptions {
    * Default: false
    */
   oneOf?: boolean;
+
+  /**
+   * Choose the type of nullability you would like to see.
+   *
+   * - TRADITIONAL: all GraphQLSemanticNonNull will be unwrapped
+   * - FULL: the true nullability will be returned
+   *
+   */
+  nullability?: 'TRADITIONAL' | 'FULL';
 }
 
 /**
@@ -52,6 +61,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
     schemaDescription: false,
     inputValueDeprecation: false,
     oneOf: false,
+    nullability: 'TRADITIONAL',
     ...options,
   };
 
@@ -70,6 +80,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 +116,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string {
         args${inputDeprecation('(includeDeprecated: true)')} {
           ...InputValue
         }
-        type {
+        type${nullability === 'FULL' ? `(nullability: ${nullability})` : ''} {
           ...TypeRef
         }
         isDeprecated
@@ -285,11 +296,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/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 287be40bfe..5b7c498c65 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);
@@ -47,13 +53,22 @@ 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;
   }
-  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/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 },
+          ],
+        },
+      ]);
+    });
+  });
 });
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;
   }