Repository: graphql/graphql-js
Pull request #3250 (2021-08-27 13:56:29)

Bring `visitorKeys` back
See https://github.com/graphql/graphql-js/issues/3245#issuecomment-907177003

I checked `Allow edits by maintainers`. So you can make changes on this PR instead of creating a new one by checking out this branch using GitHub CLI. Also you can leave comments here then I can also do the changes you want :)





yaacovCR: Returning false means you no longer visit any of the children, what if you want to visit only some?

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index dd2b7622..31ff6398 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -627,6 +627,7 @@ overrides:
     rules:
       internal-rules/require-to-string-tag: off
       node/no-unpublished-import: [error, { allowModules: ['chai', 'mocha'] }]
+      import/no-deprecated: off
       import/no-restricted-paths: off
       import/no-extraneous-dependencies: [error, { devDependencies: true }]
   - files: 'integrationTests/*'
diff --git a/package-lock.json b/package-lock.json
index ee94bf6b..60a05d91 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "graphql",
-  "version": "16.0.0-rc.1",
+  "version": "16.0.0-rc.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "graphql",
-      "version": "16.0.0-rc.1",
+      "version": "16.0.0-rc.2",
       "license": "MIT",
       "devDependencies": {
         "@babel/core": "7.15.0",
diff --git a/package.json b/package.json
index 1abf095a..8fb25667 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "graphql",
-  "version": "16.0.0-rc.1",
+  "version": "16.0.0-rc.2",
   "description": "A Query Language and Runtime which can target any service.",
   "license": "MIT",
   "private": true,
diff --git a/resources/gen-changelog.js b/resources/gen-changelog.js
index 2439b8d7..02bb6340 100644
--- a/resources/gen-changelog.js
+++ b/resources/gen-changelog.js
@@ -12,6 +12,9 @@ const labelsConfig = {
   'PR: breaking change 💥': {
     section: 'Breaking Change 💥',
   },
+  'PR: deprecation ⚠': {
+    section: 'Deprecation ⚠',
+  },
   'PR: feature 🚀': {
     section: 'New Feature 🚀',
   },
diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts
index 0e0c5dcb..2bea7d41 100644
--- a/src/error/GraphQLError.ts
+++ b/src/error/GraphQLError.ts
@@ -198,33 +198,97 @@ export class GraphQLError extends Error {
   }
 
   toString(): string {
-    return printError(this);
+    let output = this.message;
+
+    if (this.nodes) {
+      for (const node of this.nodes) {
+        if (node.loc) {
+          output += '\n\n' + printLocation(node.loc);
+        }
+      }
+    } else if (this.source && this.locations) {
+      for (const location of this.locations) {
+        output += '\n\n' + printSourceLocation(this.source, location);
+      }
+    }
+
+    return output;
+  }
+
+  toJSON(): GraphQLFormattedError {
+    type WritableFormattedError = {
+      -readonly [P in keyof GraphQLFormattedError]: GraphQLFormattedError[P];
+    };
+
+    const formattedError: WritableFormattedError = {
+      message: this.message,
+    };
+
+    if (this.locations != null) {
+      formattedError.locations = this.locations;
+    }
+
+    if (this.path != null) {
+      formattedError.path = this.path;
+    }
+
+    if (this.extensions != null) {
+      formattedError.extensions = this.extensions;
+    }
+
+    return formattedError;
   }
 
-  // FIXME: workaround to not break chai comparisons, should be remove in v16
   get [Symbol.toStringTag](): string {
-    return 'Object';
+    return 'GraphQLError';
   }
 }
 
+/**
+ * See: https://spec.graphql.org/draft/#sec-Errors
+ */
+export interface GraphQLFormattedError {
+  /**
+   * A short, human-readable summary of the problem that **SHOULD NOT** change
+   * from occurrence to occurrence of the problem, except for purposes of
+   * localization.
+   */
+  readonly message: string;
+  /**
+   * If an error can be associated to a particular point in the requested
+   * GraphQL document, it should contain a list of locations.
+   */
+  readonly locations?: ReadonlyArray<SourceLocation>;
+  /**
+   * If an error can be associated to a particular field in the GraphQL result,
+   * it _must_ contain an entry with the key `path` that details the path of
+   * the response field which experienced the error. This allows clients to
+   * identify whether a null result is intentional or caused by a runtime error.
+   */
+  readonly path?: ReadonlyArray<string | number>;
+  /**
+   * Reserved for implementors to extend the protocol however they see fit,
+   * and hence there are no additional restrictions on its contents.
+   */
+  readonly extensions?: { [key: string]: unknown };
+}
+
 /**
  * Prints a GraphQLError to a string, representing useful location information
  * about the error's position in the source.
+ *
+ * @deprecated Please use `error.toString` instead. Will be removed in v17
  */
 export function printError(error: GraphQLError): string {
-  let output = error.message;
-
-  if (error.nodes) {
-    for (const node of error.nodes) {
-      if (node.loc) {
-        output += '\n\n' + printLocation(node.loc);
-      }
-    }
-  } else if (error.source && error.locations) {
-    for (const location of error.locations) {
-      output += '\n\n' + printSourceLocation(error.source, location);
-    }
-  }
+  return error.toString();
+}
 
-  return output;
+/**
+ * Given a GraphQLError, format it according to the rules described by the
+ * Response Format, Errors section of the GraphQL Specification.
+ *
+ * @deprecated Please use `error.toString` instead. Will be removed in v17
+ */
+export function formatError(error: GraphQLError): GraphQLFormattedError {
+  return error.toJSON();
 }
diff --git a/src/error/formatError.ts b/src/error/formatError.ts
deleted file mode 100644
index 259f24e5..00000000
--- a/src/error/formatError.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { devAssert } from '../jsutils/devAssert';
-
-import type { SourceLocation } from '../language/location';
-
-import type { GraphQLError } from './GraphQLError';
-
-/**
- * Given a GraphQLError, format it according to the rules described by the
- * Response Format, Errors section of the GraphQL Specification.
- */
-export function formatError(error: GraphQLError): GraphQLFormattedError {
-  devAssert(error, 'Received null or undefined error.');
-  const message = error.message ?? 'An unknown error occurred.';
-  const locations = error.locations;
-  const path = error.path;
-  const extensions = error.extensions;
-
-  return extensions
-    ? { message, locations, path, extensions }
-    : { message, locations, path };
-}
-
-/**
- * See: https://spec.graphql.org/draft/#sec-Errors
- */
-export interface GraphQLFormattedError {
-  /**
-   * A short, human-readable summary of the problem that **SHOULD NOT** change
-   * from occurrence to occurrence of the problem, except for purposes of
-   * localization.
-   */
-  readonly message: string;
-  /**
-   * If an error can be associated to a particular point in the requested
-   * GraphQL document, it should contain a list of locations.
-   */
-  readonly locations?: ReadonlyArray<SourceLocation>;
-  /**
-   * If an error can be associated to a particular field in the GraphQL result,
-   * it _must_ contain an entry with the key `path` that details the path of
-   * the response field which experienced the error. This allows clients to
-   * identify whether a null result is intentional or caused by a runtime error.
-   */
-  readonly path?: ReadonlyArray<string | number>;
-  /**
-   * Reserved for implementors to extend the protocol however they see fit,
-   * and hence there are no additional restrictions on its contents.
-   */
-  readonly extensions?: { [key: string]: unknown };
-}
diff --git a/src/error/index.ts b/src/error/index.ts
index 914a6dbe..b947c93f 100644
--- a/src/error/index.ts
+++ b/src/error/index.ts
@@ -1,8 +1,6 @@
-export { GraphQLError, printError } from './GraphQLError';
+export { GraphQLError, printError, formatError } from './GraphQLError';
+export type { GraphQLFormattedError } from './GraphQLError';
 
 export { syntaxError } from './syntaxError';
 
 export { locatedError } from './locatedError';
-
-export { formatError } from './formatError';
-export type { GraphQLFormattedError } from './formatError';
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 8429cae8..1ef3adc8 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -13,7 +13,7 @@ import { promiseForObject } from '../jsutils/promiseForObject';
 import { addPath, pathToArray } from '../jsutils/Path';
 import { isIterableObject } from '../jsutils/isIterableObject';
 
-import type { GraphQLFormattedError } from '../error/formatError';
+import type { GraphQLFormattedError } from '../error/GraphQLError';
 import { GraphQLError } from '../error/GraphQLError';
 import { locatedError } from '../error/locatedError';
 
diff --git a/src/index.ts b/src/index.ts
index be0eda9b..836ca22d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -234,6 +234,7 @@ export type {
   /** Visitor utilities */
   ASTVisitor,
   ASTVisitFn,
+  ASTVisitorKeyMap,
   /** AST nodes */
   ASTNode,
   ASTKindToNode,
diff --git a/src/language/index.ts b/src/language/index.ts
index dfe4e535..070ca45d 100644
--- a/src/language/index.ts
+++ b/src/language/index.ts
@@ -19,7 +19,7 @@ export type { ParseOptions } from './parser';
 export { print } from './printer';
 
 export { visit, visitInParallel, getVisitFn, BREAK } from './visitor';
-export type { ASTVisitor, ASTVisitFn } from './visitor';
+export type { ASTVisitor, ASTVisitFn, ASTVisitorKeyMap } from './visitor';
 
 export { Location, Token } from './ast';
 export type {
diff --git a/src/language/lexer.ts b/src/language/lexer.ts
index 21e87252..c6f24092 100644
--- a/src/language/lexer.ts
+++ b/src/language/lexer.ts
@@ -489,12 +489,11 @@ function readDigits(lexer: Lexer, start: number, firstCode: number): number {
   }
 
   const body = lexer.source.body;
-  let position = start;
-  let code = firstCode;
+  let position = start + 1; // +1 to skip first firstCode
 
-  do {
-    code = body.charCodeAt(++position);
-  } while (isDigit(code));
+  while (isDigit(body.charCodeAt(position))) {
+    ++position;
+  }
 
   return position;
 }
diff --git a/src/language/visitor.ts b/src/language/visitor.ts
index 458a7af2..df3081be 100644
--- a/src/language/visitor.ts
+++ b/src/language/visitor.ts
@@ -76,9 +76,14 @@ type ReducedField<T, R> = T extends null | undefined
   ? ReadonlyArray<R>
   : R;
 
-const QueryDocumentKeys = {
-  Name: [],
+/**
+ * A KeyMap describes each the traversable properties of each kind of node.
+ */
+export type ASTVisitorKeyMap = {
+  [P in keyof ASTKindToNode]?: ReadonlyArray<keyof ASTKindToNode[P]>;
+};
 
+export const QueryDocumentKeys: ASTVisitorKeyMap = {
   Document: ['definitions'],
   OperationDefinition: [
     'name',
@@ -103,12 +108,6 @@ const QueryDocumentKeys = {
     'selectionSet',
   ],
 
-  IntValue: [],
-  FloatValue: [],
-  StringValue: [],
-  BooleanValue: [],
-  NullValue: [],
-  EnumValue: [],
   ListValue: ['values'],
   ObjectValue: ['fields'],
   ObjectField: ['name', 'value'],
@@ -242,11 +241,20 @@ export const BREAK: unknown = Object.freeze({});
  * })
  * ```
  */
-export function visit<N extends ASTNode>(root: N, visitor: ASTVisitor): N;
-export function visit<R>(root: ASTNode, visitor: ASTReducer<R>): R;
+export function visit<N extends ASTNode>(
+  root: N,
+  visitor: ASTVisitor,
+  visitorKeys?: ASTVisitorKeyMap,
+): N;
+export function visit<R>(
+  root: ASTNode,
+  visitor: ASTReducer<R>,
+  visitorKeys?: ASTVisitorKeyMap,
+): R;
 export function visit(
   root: ASTNode,
   visitor: ASTVisitor | ASTReducer<any>,
+  visitorKeys: ASTVisitorKeyMap = QueryDocumentKeys,
 ): any {
   /* eslint-disable no-undef-init */
   let stack: any = undefined;
@@ -346,7 +354,7 @@ export function visit(
     } else {
       stack = { inArray, index, keys, edits, prev: stack };
       inArray = Array.isArray(node);
-      keys = inArray ? node : (QueryDocumentKeys as any)[node.kind] ?? [];
+      keys = inArray ? node : (visitorKeys as any)[node.kind] ?? [];
       index = -1;
       edits = [];
       if (parent) {
diff --git a/src/type/definition.ts b/src/type/definition.ts
index 6e5f0fe4..5372bbd4 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -965,12 +965,13 @@ export type GraphQLFieldResolver<
   TSource,
   TContext,
   TArgs = { [argument: string]: any },
+  TResult = unknown,
 > = (
   source: TSource,
   args: TArgs,
   context: TContext,
   info: GraphQLResolveInfo,
-) => unknown;
+) => TResult;
 
 export interface GraphQLResolveInfo {
   readonly fieldName: string;
@@ -1648,6 +1649,7 @@ export class GraphQLInputObjectType {
       description: field.description,
       type: field.type,
       defaultValue: field.defaultValue,
+      deprecationReason: field.deprecationReason,
       extensions: field.extensions,
       astNode: field.astNode,
     }));
diff --git a/src/version.ts b/src/version.ts
index 19343173..732a274a 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -4,7 +4,7 @@
 /**
  * A string containing the version of the GraphQL.js library
  */
-export const version = '16.0.0-rc.1';
+export const version = '16.0.0-rc.2';
 
 /**
  * An object containing the components of the GraphQL.js version string
@@ -13,5 +13,5 @@ export const versionInfo = Object.freeze({
   major: 16,
   minor: 0,
   patch: 0,
-  preReleaseTag: 'rc.1',
+  preReleaseTag: 'rc.2',
 });


Test patch:
diff --git a/integrationTests/integration-test.js b/integrationTests/integration-test.js
index a2fe883b..aa5eac2f 100644
--- a/integrationTests/integration-test.js
+++ b/integrationTests/integration-test.js
@@ -28,22 +28,21 @@ describe('Integration Tests', () => {
   );
 
   function testOnNodeProject(projectName) {
-    exec(`cp -R ${path.join(__dirname, projectName)} ${tmpDir}`);
+    const projectPath = path.join(__dirname, projectName);
 
-    const cwd = path.join(tmpDir, projectName);
-    exec('npm --quiet install', { cwd, stdio: 'inherit' });
-    exec('npm --quiet test', { cwd, stdio: 'inherit' });
-  }
+    const packageJSONPath = path.join(projectPath, 'package.json');
+    const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf-8'));
 
-  it('Should compile with all supported TS versions', () => {
-    testOnNodeProject('ts');
-  }).timeout(40000);
+    it(packageJSON.description, () => {
+      exec(`cp -R ${projectPath} ${tmpDir}`);
 
-  it('Should work on all supported node versions', () => {
-    testOnNodeProject('node');
-  }).timeout(40000);
+      const cwd = path.join(tmpDir, projectName);
+      exec('npm --quiet install', { cwd, stdio: 'inherit' });
+      exec('npm --quiet test', { cwd, stdio: 'inherit' });
+    }).timeout(60000);
+  }
 
-  it('Should be compatible with Webpack', () => {
-    testOnNodeProject('webpack');
-  }).timeout(40000);
+  testOnNodeProject('ts');
+  testOnNodeProject('node');
+  testOnNodeProject('webpack');
 });
diff --git a/integrationTests/node/package.json b/integrationTests/node/package.json
index 11006007..c5f85cd8 100644
--- a/integrationTests/node/package.json
+++ b/integrationTests/node/package.json
@@ -1,5 +1,6 @@
 {
   "private": true,
+  "description": "graphql-js should work on all supported node versions",
   "scripts": {
     "test": "node test.js"
   },
diff --git a/integrationTests/ts/package.json b/integrationTests/ts/package.json
index 8b036770..3e1a3e33 100644
--- a/integrationTests/ts/package.json
+++ b/integrationTests/ts/package.json
@@ -1,5 +1,6 @@
 {
   "private": true,
+  "description": "graphql-js should compile with all supported TS versions",
   "scripts": {
     "test": "node test.js"
   },
diff --git a/integrationTests/webpack/package.json b/integrationTests/webpack/package.json
index dac5558f..aec7a21a 100644
--- a/integrationTests/webpack/package.json
+++ b/integrationTests/webpack/package.json
@@ -1,5 +1,6 @@
 {
   "private": true,
+  "description": "graphql-js should be compatible with Webpack",
   "scripts": {
     "test": "webpack && node test.js"
   },
diff --git a/src/__testUtils__/expectJSON.ts b/src/__testUtils__/expectJSON.ts
new file mode 100644
index 00000000..04a2bdb2
--- /dev/null
+++ b/src/__testUtils__/expectJSON.ts
@@ -0,0 +1,40 @@
+import { expect } from 'chai';
+
+import { mapValue } from '../jsutils/mapValue';
+import { isObjectLike } from '../jsutils/isObjectLike';
+
+/**
+ * Deeply transforms an arbitrary value to a JSON-safe value by calling toJSON
+ * on any nested value which defines it.
+ */
+function toJSONDeep(value: unknown): unknown {
+  if (!isObjectLike(value)) {
+    return value;
+  }
+
+  if (typeof value.toJSON === 'function') {
+    return value.toJSON();
+  }
+
+  if (Array.isArray(value)) {
+    return value.map(toJSONDeep);
+  }
+
+  return mapValue(value, toJSONDeep);
+}
+
+export function expectJSON(value: unknown) {
+  return expect(toJSONDeep(value));
+}
+
+export function expectToThrowJSON(fn: () => unknown) {
+  function mapException(): unknown {
+    try {
+      return fn();
+    } catch (error) {
+      throw toJSONDeep(error);
+    }
+  }
+
+  return expect(mapException).to.throw();
+}
diff --git a/src/__tests__/starWarsQuery-test.ts b/src/__tests__/starWarsQuery-test.ts
index 73f583fc..d4428068 100644
--- a/src/__tests__/starWarsQuery-test.ts
+++ b/src/__tests__/starWarsQuery-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../__testUtils__/expectJSON';
+
 import { graphql } from '../graphql';
 
 import { StarWarsSchema as schema } from './starWarsSchema';
@@ -393,7 +395,7 @@ describe('Star Wars Query Tests', () => {
       `;
 
       const result = await graphql({ schema, source });
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           hero: {
             name: 'R2-D2',
@@ -424,7 +426,7 @@ describe('Star Wars Query Tests', () => {
       `;
 
       const result = await graphql({ schema, source });
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           hero: {
             name: 'R2-D2',
@@ -475,7 +477,7 @@ describe('Star Wars Query Tests', () => {
       `;
 
       const result = await graphql({ schema, source });
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           mainHero: {
             name: 'R2-D2',
diff --git a/src/error/__tests__/GraphQLError-test.ts b/src/error/__tests__/GraphQLError-test.ts
index be349dda..ca567971 100644
--- a/src/error/__tests__/GraphQLError-test.ts
+++ b/src/error/__tests__/GraphQLError-test.ts
@@ -9,7 +9,7 @@ import { Kind } from '../../language/kinds';
 import { parse } from '../../language/parser';
 import { Source } from '../../language/source';
 
-import { GraphQLError, printError } from '../GraphQLError';
+import { GraphQLError, printError, formatError } from '../GraphQLError';
 
 const source = new Source(dedent`
   {
@@ -132,10 +132,15 @@ describe('GraphQLError', () => {
   });
 });
 
-describe('printError', () => {
+describe('toString', () => {
+  it('Deprecated: prints an error using printError', () => {
+    const error = new GraphQLError('Error');
+    expect(printError(error)).to.equal('Error');
+  });
+
   it('prints an error without location', () => {
     const error = new GraphQLError('Error without location');
-    expect(printError(error)).to.equal('Error without location');
+    expect(error.toString()).to.equal('Error without location');
   });
 
   it('prints an error using node without location', () => {
@@ -143,7 +148,7 @@ describe('printError', () => {
       'Error attached to node without location',
       parse('{ foo }', { noLocation: true }),
     );
-    expect(printError(error)).to.equal(
+    expect(error.toString()).to.equal(
       'Error attached to node without location',
     );
   });
@@ -182,7 +187,7 @@ describe('printError', () => {
       fieldB.type,
     ]);
 
-    expect(printError(error)).to.equal(dedent`
+    expect(error.toString()).to.equal(dedent`
       Example error with two nodes
 
       SourceA:2:10
@@ -199,3 +204,37 @@ describe('printError', () => {
     `);
   });
 });
+
+describe('toJSON', () => {
+  it('Deprecated: format an error using formatError', () => {
+    const error = new GraphQLError('Example Error');
+    expect(formatError(error)).to.deep.equal({
+      message: 'Example Error',
+    });
+  });
+
+  it('includes path', () => {
+    const error = new GraphQLError('msg', null, null, null, [
+      'path',
+      3,
+      'to',
+      'field',
+    ]);
+
+    expect(error.toJSON()).to.deep.equal({
+      message: 'msg',
+      path: ['path', 3, 'to', 'field'],
+    });
+  });
+
+  it('includes extension fields', () => {
+    const error = new GraphQLError('msg', null, null, null, null, null, {
+      foo: 'bar',
+    });
+
+    expect(error.toJSON()).to.deep.equal({
+      message: 'msg',
+      extensions: { foo: 'bar' },
+    });
+  });
+});
diff --git a/src/error/__tests__/formatError-test.ts b/src/error/__tests__/formatError-test.ts
deleted file mode 100644
index b0798c95..00000000
--- a/src/error/__tests__/formatError-test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-
-import { formatError } from '../formatError';
-import { GraphQLError } from '../GraphQLError';
-
-describe('formatError: default error formatter', () => {
-  it('uses default message', () => {
-    // @ts-expect-error
-    const e = new GraphQLError();
-
-    expect(formatError(e)).to.deep.equal({
-      message: 'An unknown error occurred.',
-      path: undefined,
-      locations: undefined,
-    });
-  });
-
-  it('includes path', () => {
-    const e = new GraphQLError('msg', null, null, null, [
-      'path',
-      3,
-      'to',
-      'field',
-    ]);
-
-    expect(formatError(e)).to.deep.equal({
-      message: 'msg',
-      locations: undefined,
-      path: ['path', 3, 'to', 'field'],
-    });
-  });
-
-  it('includes extension fields', () => {
-    const e = new GraphQLError('msg', null, null, null, null, null, {
-      foo: 'bar',
-    });
-
-    expect(formatError(e)).to.deep.equal({
-      message: 'msg',
-      locations: undefined,
-      path: undefined,
-      extensions: { foo: 'bar' },
-    });
-  });
-
-  it('rejects null and undefined errors', () => {
-    // @ts-expect-error (formatError expects a value)
-    expect(() => formatError(undefined)).to.throw(
-      'Received null or undefined error.',
-    );
-
-    // @ts-expect-error (formatError expects a value)
-    expect(() => formatError(null)).to.throw(
-      'Received null or undefined error.',
-    );
-  });
-});
diff --git a/src/execution/__tests__/abstract-test.ts b/src/execution/__tests__/abstract-test.ts
index 0dd097f5..18b85867 100644
--- a/src/execution/__tests__/abstract-test.ts
+++ b/src/execution/__tests__/abstract-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { parse } from '../../language/parser';
 
 import { GraphQLSchema } from '../../type/schema';
@@ -204,7 +206,7 @@ describe('Execute: Handles execution of abstract types', () => {
       }
     `;
 
-    expect(await executeQuery({ schema, query })).to.deep.equal({
+    expectJSON(await executeQuery({ schema, query })).to.deep.equal({
       data: {
         pets: [null, null],
       },
@@ -358,7 +360,7 @@ describe('Execute: Handles execution of abstract types', () => {
       }
     `;
 
-    expect(await executeQuery({ schema, query })).to.deep.equal({
+    expectJSON(await executeQuery({ schema, query })).to.deep.equal({
       data: {
         pets: [null, null],
       },
@@ -539,7 +541,7 @@ describe('Execute: Handles execution of abstract types', () => {
       const result = executeSync({ schema, document, rootValue });
       return {
         toEqual(message: string) {
-          expect(result).to.deep.equal({
+          expectJSON(result).to.deep.equal({
             data: { pet: null },
             errors: [
               {
diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts
index 330f719d..f5e55b9e 100644
--- a/src/execution/__tests__/executor-test.ts
+++ b/src/execution/__tests__/executor-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { inspect } from '../../jsutils/inspect';
 import { invariant } from '../../jsutils/invariant';
 
@@ -497,7 +499,7 @@ describe('Execute: Handles basic execution tasks', () => {
     };
 
     const result = await execute({ schema, document, rootValue });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: {
         sync: 'sync',
         syncError: null,
@@ -611,7 +613,7 @@ describe('Execute: Handles basic execution tasks', () => {
 
     const result = await execute({ schema, document });
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: { foods: null },
       errors: [
         {
@@ -670,7 +672,7 @@ describe('Execute: Handles basic execution tasks', () => {
     `);
 
     const result = executeSync({ schema, document });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: {
         nullableA: {
           aliasedA: null,
@@ -752,7 +754,7 @@ describe('Execute: Handles basic execution tasks', () => {
     const rootValue = { a: 'b' };
 
     const result = executeSync({ schema, document, rootValue });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [{ message: 'Must provide an operation.' }],
     });
   });
@@ -772,7 +774,7 @@ describe('Execute: Handles basic execution tasks', () => {
     `);
 
     const result = executeSync({ schema, document });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -798,7 +800,7 @@ describe('Execute: Handles basic execution tasks', () => {
     const operationName = 'UnknownExample';
 
     const result = executeSync({ schema, document, operationName });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [{ message: 'Unknown operation named "UnknownExample".' }],
     });
   });
@@ -816,7 +818,7 @@ describe('Execute: Handles basic execution tasks', () => {
     const operationName = '';
 
     const result = executeSync({ schema, document, operationName });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [{ message: 'Unknown operation named "".' }],
     });
   });
@@ -1074,7 +1076,7 @@ describe('Execute: Handles basic execution tasks', () => {
     };
 
     const result = executeSync({ schema, document, rootValue });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: {
         specials: [{ value: 'foo' }, null],
       },
@@ -1118,7 +1120,7 @@ describe('Execute: Handles basic execution tasks', () => {
     });
 
     const result = executeSync({ schema, document: parse('{ customScalar }') });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: { customScalar: null },
       errors: [
         {
diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts
index 53314ecb..45ca7f5d 100644
--- a/src/execution/__tests__/lists-test.ts
+++ b/src/execution/__tests__/lists-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { parse } from '../../language/parser';
 
 import { buildSchema } from '../../utilities/buildASTSchema';
@@ -50,7 +52,7 @@ describe('Execute: Accepts any iterable as list value', () => {
   it('Does not accept (Iterable) String-literal as a List value', () => {
     const listField = 'Singular';
 
-    expect(complete({ listField })).to.deep.equal({
+    expectJSON(complete({ listField })).to.deep.equal({
       data: { listField: null },
       errors: [
         {
@@ -129,11 +131,11 @@ describe('Execute: Handles list nullability', () => {
     expect(await complete({ listField, as: '[Int]!' })).to.deep.equal({
       data: { listField: [1, null, 2] },
     });
-    expect(await complete({ listField, as: '[Int!]' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]' })).to.deep.equal({
       data: { listField: null },
       errors,
     });
-    expect(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
       data: null,
       errors,
     });
@@ -152,14 +154,14 @@ describe('Execute: Handles list nullability', () => {
     expect(await complete({ listField, as: '[Int]' })).to.deep.equal({
       data: { listField: null },
     });
-    expect(await complete({ listField, as: '[Int]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int]!' })).to.deep.equal({
       data: null,
       errors,
     });
     expect(await complete({ listField, as: '[Int!]' })).to.deep.equal({
       data: { listField: null },
     });
-    expect(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
       data: null,
       errors,
     });
@@ -175,19 +177,19 @@ describe('Execute: Handles list nullability', () => {
       },
     ];
 
-    expect(await complete({ listField, as: '[Int]' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int]' })).to.deep.equal({
       data: { listField: [1, null, 2] },
       errors,
     });
-    expect(await complete({ listField, as: '[Int]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int]!' })).to.deep.equal({
       data: { listField: [1, null, 2] },
       errors,
     });
-    expect(await complete({ listField, as: '[Int!]' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]' })).to.deep.equal({
       data: { listField: null },
       errors,
     });
-    expect(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
       data: null,
       errors,
     });
@@ -203,19 +205,19 @@ describe('Execute: Handles list nullability', () => {
       },
     ];
 
-    expect(await complete({ listField, as: '[Int]' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int]' })).to.deep.equal({
       data: { listField: null },
       errors,
     });
-    expect(await complete({ listField, as: '[Int]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int]!' })).to.deep.equal({
       data: null,
       errors,
     });
-    expect(await complete({ listField, as: '[Int!]' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]' })).to.deep.equal({
       data: { listField: null },
       errors,
     });
-    expect(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
+    expectJSON(await complete({ listField, as: '[Int!]!' })).to.deep.equal({
       data: null,
       errors,
     });
diff --git a/src/execution/__tests__/mutations-test.ts b/src/execution/__tests__/mutations-test.ts
index f15977cc..4d11b11b 100644
--- a/src/execution/__tests__/mutations-test.ts
+++ b/src/execution/__tests__/mutations-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
 
 import { parse } from '../../language/parser';
@@ -167,7 +169,7 @@ describe('Execute: Handles mutation execution ordering', () => {
     const rootValue = new Root(6);
     const result = await execute({ schema, document, rootValue });
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: {
         first: { theNumber: 1 },
         second: { theNumber: 2 },
diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts
index 223c552c..4fb45890 100644
--- a/src/execution/__tests__/nonnull-test.ts
+++ b/src/execution/__tests__/nonnull-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { parse } from '../../language/parser';
 
 import { GraphQLSchema } from '../../type/schema';
@@ -130,7 +132,7 @@ async function executeSyncAndAsync(query: string, rootValue: unknown) {
     rootValue,
   });
 
-  expect(asyncResult).to.deep.equal(patchData(syncResult));
+  expectJSON(asyncResult).to.deep.equal(patchData(syncResult));
   return syncResult;
 }
 
@@ -151,7 +153,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that throws', async () => {
       const result = await executeSyncAndAsync(query, throwingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: { sync: null },
         errors: [
           {
@@ -175,7 +177,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that returns null', async () => {
       const result = await executeSyncAndAsync(query, nullingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: { syncNest: null },
         errors: [
           {
@@ -190,7 +192,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that throws', async () => {
       const result = await executeSyncAndAsync(query, throwingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: { syncNest: null },
         errors: [
           {
@@ -242,7 +244,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that throws', async () => {
       const result = await executeQuery(query, throwingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data,
         errors: [
           {
@@ -368,7 +370,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that returns null', async () => {
       const result = await executeQuery(query, nullingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data,
         errors: [
           {
@@ -429,7 +431,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that throws', async () => {
       const result = await executeQuery(query, throwingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data,
         errors: [
           {
@@ -494,7 +496,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that returns null', async () => {
       const result = await executeSyncAndAsync(query, nullingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: null,
         errors: [
           {
@@ -509,7 +511,7 @@ describe('Execute: handles non-nullable types', () => {
 
     it('that throws', async () => {
       const result = await executeSyncAndAsync(query, throwingData);
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: null,
         errors: [
           {
@@ -609,7 +611,7 @@ describe('Execute: handles non-nullable types', () => {
         `),
       });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           withNonNullArg: null,
         },
@@ -636,7 +638,7 @@ describe('Execute: handles non-nullable types', () => {
         `),
       });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           withNonNullArg: null,
         },
@@ -666,7 +668,7 @@ describe('Execute: handles non-nullable types', () => {
         },
       });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           withNonNullArg: null,
         },
@@ -694,7 +696,7 @@ describe('Execute: handles non-nullable types', () => {
         },
       });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           withNonNullArg: null,
         },
diff --git a/src/execution/__tests__/sync-test.ts b/src/execution/__tests__/sync-test.ts
index 184a259b..c270c4d9 100644
--- a/src/execution/__tests__/sync-test.ts
+++ b/src/execution/__tests__/sync-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { parse } from '../../language/parser';
 
 import { validate } from '../../validation/validate';
@@ -52,7 +54,7 @@ describe('Execute: synchronously when possible', () => {
       document: parse(doc),
       rootValue: 'rootValue',
     });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [{ message: 'Must provide an operation.' }],
     });
   });
@@ -120,7 +122,7 @@ describe('Execute: synchronously when possible', () => {
         schema: badSchema,
         source: '{ __typename }',
       });
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [{ message: 'Query root type must be provided.' }],
       });
     });
@@ -131,7 +133,7 @@ describe('Execute: synchronously when possible', () => {
         schema,
         source: doc,
       });
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message: 'Syntax Error: Expected Name, found "{".',
diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts
index ebf7d0b1..df89ab24 100644
--- a/src/execution/__tests__/variables-test.ts
+++ b/src/execution/__tests__/variables-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { inspect } from '../../jsutils/inspect';
 import { invariant } from '../../jsutils/invariant';
 
@@ -196,7 +198,7 @@ describe('Execute: Handles inputs', () => {
           }
         `);
 
-        expect(result).to.deep.equal({
+        expectJSON(result).to.deep.equal({
           data: {
             fieldWithObjectInput: null,
           },
@@ -369,7 +371,7 @@ describe('Execute: Handles inputs', () => {
         const params = { input: { a: 'foo', b: 'bar', c: null } };
         const result = executeQuery(doc, params);
 
-        expect(result).to.deep.equal({
+        expectJSON(result).to.deep.equal({
           errors: [
             {
               message:
@@ -383,7 +385,7 @@ describe('Execute: Handles inputs', () => {
       it('errors on incorrect type', () => {
         const result = executeQuery(doc, { input: 'foo bar' });
 
-        expect(result).to.deep.equal({
+        expectJSON(result).to.deep.equal({
           errors: [
             {
               message:
@@ -397,7 +399,7 @@ describe('Execute: Handles inputs', () => {
       it('errors on omission of nested non-null', () => {
         const result = executeQuery(doc, { input: { a: 'foo', b: 'bar' } });
 
-        expect(result).to.deep.equal({
+        expectJSON(result).to.deep.equal({
           errors: [
             {
               message:
@@ -416,7 +418,7 @@ describe('Execute: Handles inputs', () => {
         `;
         const result = executeQuery(nestedDoc, { input: { na: { a: 'foo' } } });
 
-        expect(result).to.deep.equal({
+        expectJSON(result).to.deep.equal({
           errors: [
             {
               message:
@@ -438,7 +440,7 @@ describe('Execute: Handles inputs', () => {
         };
         const result = executeQuery(doc, params);
 
-        expect(result).to.deep.equal({
+        expectJSON(result).to.deep.equal({
           errors: [
             {
               message:
@@ -613,7 +615,7 @@ describe('Execute: Handles inputs', () => {
         }
       `);
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -632,7 +634,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { value: null });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -675,7 +677,7 @@ describe('Execute: Handles inputs', () => {
     it('reports error for missing non-nullable inputs', () => {
       const result = executeQuery('{ fieldWithNonNullableStringInput }');
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           fieldWithNonNullableStringInput: null,
         },
@@ -698,7 +700,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { value: [1, 2, 3] });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -723,7 +725,7 @@ describe('Execute: Handles inputs', () => {
         }
       `);
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           fieldWithNonNullableStringInput: null,
         },
@@ -781,7 +783,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { input: null });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -844,7 +846,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { input: ['A', null, 'B'] });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -863,7 +865,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { input: null });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -893,7 +895,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { input: ['A', null, 'B'] });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -912,7 +914,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { input: { list: ['A', 'B'] } });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -931,7 +933,7 @@ describe('Execute: Handles inputs', () => {
       `;
       const result = executeQuery(doc, { input: 'WhoKnows' });
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           {
             message:
@@ -975,7 +977,7 @@ describe('Execute: Handles inputs', () => {
         }
       `);
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         data: {
           fieldWithDefaultArgumentValue: null,
         },
@@ -1029,7 +1031,7 @@ describe('Execute: Handles inputs', () => {
     it('return all errors by default', () => {
       const result = getVariableValues(schema, variableDefinitions, inputValue);
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           invalidValueError(0, 0),
           invalidValueError(1, 1),
@@ -1046,7 +1048,7 @@ describe('Execute: Handles inputs', () => {
         { maxErrors: 3 },
       );
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           invalidValueError(0, 0),
           invalidValueError(1, 1),
@@ -1063,7 +1065,7 @@ describe('Execute: Handles inputs', () => {
         { maxErrors: 2 },
       );
 
-      expect(result).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         errors: [
           invalidValueError(0, 0),
           invalidValueError(1, 1),
diff --git a/src/language/__tests__/lexer-test.ts b/src/language/__tests__/lexer-test.ts
index 8e26212a..173d7176 100644
--- a/src/language/__tests__/lexer-test.ts
+++ b/src/language/__tests__/lexer-test.ts
@@ -2,6 +2,7 @@ import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
 import { dedent } from '../../__testUtils__/dedent';
+import { expectToThrowJSON } from '../../__testUtils__/expectJSON';
 
 import { inspect } from '../../jsutils/inspect';
 
@@ -24,7 +25,7 @@ function lexSecond(str: string) {
 }
 
 function expectSyntaxError(text: string) {
-  return expect(() => lexSecond(text)).to.throw();
+  return expectToThrowJSON(() => lexSecond(text));
 }
 
 describe('Lexer', () => {
diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts
index 8cbfe16c..767d3d1b 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -3,6 +3,7 @@ import { describe, it } from 'mocha';
 
 import { dedent } from '../../__testUtils__/dedent';
 import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery';
+import { expectJSON, expectToThrowJSON } from '../../__testUtils__/expectJSON';
 
 import { inspect } from '../../jsutils/inspect';
 
@@ -11,10 +12,8 @@ import { Source } from '../source';
 import { TokenKind } from '../tokenKind';
 import { parse, parseValue, parseConstValue, parseType } from '../parser';
 
-import { toJSONDeep } from './toJSONDeep';
-
 function expectSyntaxError(text: string) {
-  return expect(() => parse(text)).to.throw();
+  return expectToThrowJSON(() => parse(text));
 }
 
 describe('Parser', () => {
@@ -235,7 +234,7 @@ describe('Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(result)).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       kind: Kind.DOCUMENT,
       loc: { start: 0, end: 40 },
       definitions: [
@@ -325,7 +324,7 @@ describe('Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(result)).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       kind: Kind.DOCUMENT,
       loc: { start: 0, end: 29 },
       definitions: [
@@ -420,7 +419,7 @@ describe('Parser', () => {
   describe('parseValue', () => {
     it('parses null value', () => {
       const result = parseValue('null');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.NULL,
         loc: { start: 0, end: 4 },
       });
@@ -428,7 +427,7 @@ describe('Parser', () => {
 
     it('parses list values', () => {
       const result = parseValue('[123 "abc"]');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.LIST,
         loc: { start: 0, end: 11 },
         values: [
@@ -449,7 +448,7 @@ describe('Parser', () => {
 
     it('parses block strings', () => {
       const result = parseValue('["""long""" "short"]');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.LIST,
         loc: { start: 0, end: 20 },
         values: [
@@ -471,7 +470,7 @@ describe('Parser', () => {
 
     it('allows variables', () => {
       const result = parseValue('{ field: $var }');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.OBJECT,
         loc: { start: 0, end: 15 },
         fields: [
@@ -519,7 +518,7 @@ describe('Parser', () => {
   describe('parseConstValue', () => {
     it('parses values', () => {
       const result = parseConstValue('[123 "abc"]');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.LIST,
         loc: { start: 0, end: 11 },
         values: [
@@ -561,7 +560,7 @@ describe('Parser', () => {
   describe('parseType', () => {
     it('parses well known types', () => {
       const result = parseType('String');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.NAMED_TYPE,
         loc: { start: 0, end: 6 },
         name: {
@@ -574,7 +573,7 @@ describe('Parser', () => {
 
     it('parses custom types', () => {
       const result = parseType('MyType');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.NAMED_TYPE,
         loc: { start: 0, end: 6 },
         name: {
@@ -587,7 +586,7 @@ describe('Parser', () => {
 
     it('parses list types', () => {
       const result = parseType('[MyType]');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.LIST_TYPE,
         loc: { start: 0, end: 8 },
         type: {
@@ -604,7 +603,7 @@ describe('Parser', () => {
 
     it('parses non-null types', () => {
       const result = parseType('MyType!');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.NON_NULL_TYPE,
         loc: { start: 0, end: 7 },
         type: {
@@ -621,7 +620,7 @@ describe('Parser', () => {
 
     it('parses nested types', () => {
       const result = parseType('[MyType!]');
-      expect(toJSONDeep(result)).to.deep.equal({
+      expectJSON(result).to.deep.equal({
         kind: Kind.LIST_TYPE,
         loc: { start: 0, end: 9 },
         type: {
diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts
index 3ad3b949..46fed5fb 100644
--- a/src/language/__tests__/schema-parser-test.ts
+++ b/src/language/__tests__/schema-parser-test.ts
@@ -3,13 +3,12 @@ import { describe, it } from 'mocha';
 
 import { dedent } from '../../__testUtils__/dedent';
 import { kitchenSinkSDL } from '../../__testUtils__/kitchenSinkSDL';
+import { expectJSON, expectToThrowJSON } from '../../__testUtils__/expectJSON';
 
 import { parse } from '../parser';
 
-import { toJSONDeep } from './toJSONDeep';
-
 function expectSyntaxError(text: string) {
-  return expect(() => parse(text)).to.throw();
+  return expectToThrowJSON(() => parse(text));
 }
 
 function typeNode(name: unknown, loc: unknown) {
@@ -84,7 +83,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -115,15 +114,12 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.nested.property(
-      'definitions[0].description',
-      {
-        kind: 'StringValue',
-        value: 'Description',
-        block: false,
-        loc: { start: 0, end: 13 },
-      },
-    );
+    expectJSON(doc).to.deep.nested.property('definitions[0].description', {
+      kind: 'StringValue',
+      value: 'Description',
+      block: false,
+      loc: { start: 0, end: 13 },
+    });
   });
 
   it('parses type with description multi-line string', () => {
@@ -137,15 +133,12 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.nested.property(
-      'definitions[0].description',
-      {
-        kind: 'StringValue',
-        value: 'Description',
-        block: true,
-        loc: { start: 0, end: 19 },
-      },
-    );
+    expectJSON(doc).to.deep.nested.property('definitions[0].description', {
+      kind: 'StringValue',
+      value: 'Description',
+      block: true,
+      loc: { start: 0, end: 19 },
+    });
   });
 
   it('parses schema with description string', () => {
@@ -156,15 +149,12 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.nested.property(
-      'definitions[0].description',
-      {
-        kind: 'StringValue',
-        value: 'Description',
-        block: false,
-        loc: { start: 0, end: 13 },
-      },
-    );
+    expectJSON(doc).to.deep.nested.property('definitions[0].description', {
+      kind: 'StringValue',
+      value: 'Description',
+      block: false,
+      loc: { start: 0, end: 13 },
+    });
   });
 
   it('Description followed by something other than type system definition throws', () => {
@@ -181,7 +171,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -206,7 +196,7 @@ describe('Schema Parser', () => {
   it('Object extension without fields', () => {
     const doc = parse('extend type Hello implements Greeting');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -224,7 +214,7 @@ describe('Schema Parser', () => {
 
   it('Interface extension without fields', () => {
     const doc = parse('extend interface Hello implements Greeting');
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -247,7 +237,7 @@ describe('Schema Parser', () => {
       extend type Hello implements SecondGreeting
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -309,7 +299,7 @@ describe('Schema Parser', () => {
 
       extend interface Hello implements SecondGreeting
     `);
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -383,7 +373,7 @@ describe('Schema Parser', () => {
         mutation: Mutation
       }`;
     const doc = parse(body);
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -407,7 +397,7 @@ describe('Schema Parser', () => {
   it('Schema extension with only directives', () => {
     const body = 'extend schema @directive';
     const doc = parse(body);
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -449,7 +439,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -478,7 +468,7 @@ describe('Schema Parser', () => {
 
   it('Simple interface inheriting interface', () => {
     const doc = parse('interface Hello implements World { field: String }');
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -504,7 +494,7 @@ describe('Schema Parser', () => {
   it('Simple type inheriting interface', () => {
     const doc = parse('type Hello implements World { field: String }');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -530,7 +520,7 @@ describe('Schema Parser', () => {
   it('Simple type inheriting multiple interfaces', () => {
     const doc = parse('type Hello implements Wo & rld { field: String }');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -558,7 +548,7 @@ describe('Schema Parser', () => {
 
   it('Simple interface inheriting multiple interfaces', () => {
     const doc = parse('interface Hello implements Wo & rld { field: String }');
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -587,7 +577,7 @@ describe('Schema Parser', () => {
   it('Simple type inheriting multiple interfaces with leading ampersand', () => {
     const doc = parse('type Hello implements & Wo & rld { field: String }');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -617,7 +607,7 @@ describe('Schema Parser', () => {
     const doc = parse(
       'interface Hello implements & Wo & rld { field: String }',
     );
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -646,7 +636,7 @@ describe('Schema Parser', () => {
   it('Single value enum', () => {
     const doc = parse('enum Hello { WORLD }');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -665,7 +655,7 @@ describe('Schema Parser', () => {
   it('Double value enum', () => {
     const doc = parse('enum Hello { WO, RLD }');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -691,7 +681,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -721,7 +711,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -759,7 +749,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -801,7 +791,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -843,7 +833,7 @@ describe('Schema Parser', () => {
       }
     `);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -883,7 +873,7 @@ describe('Schema Parser', () => {
   it('Simple union', () => {
     const doc = parse('union Hello = World');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -902,7 +892,7 @@ describe('Schema Parser', () => {
   it('Union with two types', () => {
     const doc = parse('union Hello = Wo | Rld');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -924,7 +914,7 @@ describe('Schema Parser', () => {
   it('Union with two types and leading pipe', () => {
     const doc = parse('union Hello = | Wo | Rld');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -974,7 +964,7 @@ describe('Schema Parser', () => {
   it('Scalar', () => {
     const doc = parse('scalar Hello');
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -995,7 +985,7 @@ input Hello {
   world: String
 }`);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -1033,7 +1023,7 @@ input Hello {
     const body = 'directive @foo on OBJECT | INTERFACE';
     const doc = parse(body);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
@@ -1069,7 +1059,7 @@ input Hello {
     const body = 'directive @foo repeatable on OBJECT | INTERFACE';
     const doc = parse(body);
 
-    expect(toJSONDeep(doc)).to.deep.equal({
+    expectJSON(doc).to.deep.equal({
       kind: 'Document',
       definitions: [
         {
diff --git a/src/language/__tests__/toJSONDeep.ts b/src/language/__tests__/toJSONDeep.ts
deleted file mode 100644
index a8260625..00000000
--- a/src/language/__tests__/toJSONDeep.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { mapValue } from '../../jsutils/mapValue';
-import { isObjectLike } from '../../jsutils/isObjectLike';
-
-/**
- * Deeply transforms an arbitrary value to a JSON-safe value by calling toJSON
- * on any nested value which defines it.
- */
-export function toJSONDeep(value: unknown): unknown {
-  if (!isObjectLike(value)) {
-    return value;
-  }
-
-  if (typeof value.toJSON === 'function') {
-    return value.toJSON();
-  }
-
-  if (Array.isArray(value)) {
-    return value.map(toJSONDeep);
-  }
-
-  return mapValue(value, toJSONDeep);
-}
diff --git a/src/language/__tests__/visitor-test.ts b/src/language/__tests__/visitor-test.ts
index 93b8fe07..e63f03ed 100644
--- a/src/language/__tests__/visitor-test.ts
+++ b/src/language/__tests__/visitor-test.ts
@@ -4,6 +4,7 @@ import { describe, it } from 'mocha';
 import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery';
 
 import type { ASTNode, SelectionSetNode } from '../ast';
+import type { ASTVisitorKeyMap, ASTVisitor } from '../visitor';
 import { isNode } from '../ast';
 import { Kind } from '../kinds';
 import { parse } from '../parser';
@@ -462,6 +463,41 @@ describe('Visitor', () => {
     ]);
   });
 
+  it('visits only the specified `Kind` in visitorKeyMap', () => {
+    const visited: Array<any> = [];
+
+    const visitorKeyMap: ASTVisitorKeyMap = {
+      Document: ['definitions'],
+      OperationDefinition: ['name'],
+    };
+
+    const visitor: ASTVisitor = {
+      enter(node) {
+        visited.push(['enter', node.kind, getValue(node)]);
+      },
+      leave(node) {
+        visited.push(['leave', node.kind, getValue(node)]);
+      },
+    };
+
+    const exampleDocumentAST = parse(`
+      query ExampleOperation {
+        someField
+      }
+    `);
+
+    visit(exampleDocumentAST, visitor, visitorKeyMap);
+
+    expect(visited).to.deep.equal([
+      ['enter', 'Document', undefined],
+      ['enter', 'OperationDefinition', undefined],
+      ['enter', 'Name', 'ExampleOperation'],
+      ['leave', 'Name', 'ExampleOperation'],
+      ['leave', 'OperationDefinition', undefined],
+      ['leave', 'Document', undefined],
+    ]);
+  });
+
   it('Legacy: visits variables defined in fragments', () => {
     const ast = parse('fragment a($v: Boolean = false) on t { f }', {
       noLocation: true,
diff --git a/src/subscription/__tests__/subscribe-test.ts b/src/subscription/__tests__/subscribe-test.ts
index 85d6400a..00442963 100644
--- a/src/subscription/__tests__/subscribe-test.ts
+++ b/src/subscription/__tests__/subscribe-test.ts
@@ -1,6 +1,7 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
 import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
 
 import { invariant } from '../../jsutils/invariant';
@@ -347,7 +348,7 @@ describe('Subscription Initialization Phase', () => {
     const document = parse('subscription { unknownField }');
 
     const result = await subscribe({ schema, document });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message: 'The subscription field "unknownField" is not defined.',
@@ -423,24 +424,24 @@ describe('Subscription Initialization Phase', () => {
       ],
     };
 
-    expect(
+    expectJSON(
       // Returning an error
       await subscribeWithFn(() => new Error('test error')),
     ).to.deep.equal(expectedResult);
 
-    expect(
+    expectJSON(
       // Throwing an error
       await subscribeWithFn(() => {
         throw new Error('test error');
       }),
     ).to.deep.equal(expectedResult);
 
-    expect(
+    expectJSON(
       // Resolving to an error
       await subscribeWithFn(() => Promise.resolve(new Error('test error'))),
     ).to.deep.equal(expectedResult);
 
-    expect(
+    expectJSON(
       // Rejecting with an error
       await subscribeWithFn(() => Promise.reject(new Error('test error'))),
     ).to.deep.equal(expectedResult);
@@ -470,7 +471,7 @@ describe('Subscription Initialization Phase', () => {
     // If we receive variables that cannot be coerced correctly, subscribe() will
     // resolve to an ExecutionResult that contains an informative error description.
     const result = await subscribe({ schema, document, variableValues });
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -892,7 +893,7 @@ describe('Subscription Publish Phase', () => {
     });
 
     // An error in execution is presented as such.
-    expect(await subscription.next()).to.deep.equal({
+    expectJSON(await subscription.next()).to.deep.equal({
       done: false,
       value: {
         data: { newMessage: null },
@@ -908,7 +909,7 @@ describe('Subscription Publish Phase', () => {
 
     // However that does not close the response event stream.
     // Subsequent events are still executed.
-    expect(await subscription.next()).to.deep.equal({
+    expectJSON(await subscription.next()).to.deep.equal({
       done: false,
       value: {
         data: { newMessage: 'Bonjour' },
diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts
index 1cf4e4f3..7011937c 100644
--- a/src/type/__tests__/definition-test.ts
+++ b/src/type/__tests__/definition-test.ts
@@ -818,6 +818,22 @@ describe('Type System: Input Objects', () => {
       );
     });
   });
+
+  it('Deprecation reason is preserved on fields', () => {
+    const inputObjType = new GraphQLInputObjectType({
+      name: 'SomeInputObject',
+      fields: {
+        deprecatedField: {
+          type: ScalarType,
+          deprecationReason: 'not used anymore',
+        },
+      },
+    });
+    expect(inputObjType.toConfig()).to.have.nested.property(
+      'fields.deprecatedField.deprecationReason',
+      'not used anymore',
+    );
+  });
 });
 
 describe('Type System: List', () => {
diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts
index c3cf23cd..301eb138 100644
--- a/src/type/__tests__/enumType-test.ts
+++ b/src/type/__tests__/enumType-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { graphqlSync } from '../../graphql';
 import { introspectionFromSchema } from '../../utilities/introspectionFromSchema';
 
@@ -147,7 +149,7 @@ describe('Type System: Enum Values', () => {
   it('does not accept string literals', () => {
     const result = executeQuery('{ colorEnum(fromEnum: "GREEN") }');
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -161,7 +163,7 @@ describe('Type System: Enum Values', () => {
   it('does not accept values not in the enum', () => {
     const result = executeQuery('{ colorEnum(fromEnum: GREENISH) }');
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -175,7 +177,7 @@ describe('Type System: Enum Values', () => {
   it('does not accept values with incorrect casing', () => {
     const result = executeQuery('{ colorEnum(fromEnum: green) }');
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -189,7 +191,7 @@ describe('Type System: Enum Values', () => {
   it('does not accept incorrect internal value', () => {
     const result = executeQuery('{ colorEnum(fromString: "GREEN") }');
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: { colorEnum: null },
       errors: [
         {
@@ -204,7 +206,7 @@ describe('Type System: Enum Values', () => {
   it('does not accept internal value in place of enum literal', () => {
     const result = executeQuery('{ colorEnum(fromEnum: 1) }');
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message: 'Enum "Color" cannot represent non-enum value: 1.',
@@ -217,7 +219,7 @@ describe('Type System: Enum Values', () => {
   it('does not accept enum literal in place of int', () => {
     const result = executeQuery('{ colorEnum(fromInt: GREEN) }');
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message: 'Int cannot represent non-integer value: GREEN',
@@ -259,7 +261,7 @@ describe('Type System: Enum Values', () => {
     const doc = 'query ($color: Color!) { colorEnum(fromEnum: $color) }';
     const result = executeQuery(doc, { color: 2 });
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -274,7 +276,7 @@ describe('Type System: Enum Values', () => {
     const doc = 'query ($color: String!) { colorEnum(fromEnum: $color) }';
     const result = executeQuery(doc, { color: 'BLUE' });
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -292,7 +294,7 @@ describe('Type System: Enum Values', () => {
     const doc = 'query ($color: Int!) { colorEnum(fromEnum: $color) }';
     const result = executeQuery(doc, { color: 2 });
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       errors: [
         {
           message:
@@ -379,7 +381,7 @@ describe('Type System: Enum Values', () => {
       }
     `);
 
-    expect(result).to.deep.equal({
+    expectJSON(result).to.deep.equal({
       data: {
         first: 'ONE',
         second: 'TWO',
diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts
index 4d5f1398..0851769d 100644
--- a/src/type/__tests__/introspection-test.ts
+++ b/src/type/__tests__/introspection-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { buildSchema } from '../../utilities/buildASTSchema';
 import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery';
 
@@ -1522,7 +1524,7 @@ describe('Introspection', () => {
       }
     `;
 
-    expect(graphqlSync({ schema, source })).to.deep.equal({
+    expectJSON(graphqlSync({ schema, source })).to.deep.equal({
       errors: [
         {
           message:
diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts
index b3602105..016ff8a7 100644
--- a/src/type/__tests__/validation-test.ts
+++ b/src/type/__tests__/validation-test.ts
@@ -2,6 +2,7 @@ import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
 import { dedent } from '../../__testUtils__/dedent';
+import { expectJSON } from '../../__testUtils__/expectJSON';
 
 import { inspect } from '../../jsutils/inspect';
 
@@ -121,7 +122,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
 
     const schemaWithDef = buildSchema(`
       schema {
@@ -132,7 +133,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([]);
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([]);
   });
 
   it('accepts a Schema whose query and mutation types are object types', () => {
@@ -145,7 +146,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
 
     const schemaWithDef = buildSchema(`
       schema {
@@ -161,7 +162,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([]);
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([]);
   });
 
   it('accepts a Schema whose query and subscription types are object types', () => {
@@ -174,7 +175,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
 
     const schemaWithDef = buildSchema(`
       schema {
@@ -190,7 +191,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([]);
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([]);
   });
 
   it('rejects a Schema without a query type', () => {
@@ -199,7 +200,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Query root type must be provided.',
       },
@@ -214,7 +215,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([
       {
         message: 'Query root type must be provided.',
         locations: [{ line: 2, column: 7 }],
@@ -228,7 +229,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Query root type must be Object type, it cannot be Query.',
         locations: [{ line: 2, column: 7 }],
@@ -244,7 +245,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([
       {
         message:
           'Query root type must be Object type, it cannot be SomeInputObject.',
@@ -263,7 +264,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Mutation root type must be Object type if provided, it cannot be Mutation.',
@@ -285,7 +286,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([
       {
         message:
           'Mutation root type must be Object type if provided, it cannot be SomeInputObject.',
@@ -304,7 +305,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Subscription root type must be Object type if provided, it cannot be Subscription.',
@@ -326,7 +327,7 @@ describe('Type System: A Schema must have Object root types', () => {
         test: String
       }
     `);
-    expect(validateSchema(schemaWithDef)).to.deep.equal([
+    expectJSON(validateSchema(schemaWithDef)).to.deep.equal([
       {
         message:
           'Subscription root type must be Object type if provided, it cannot be SomeInputObject.',
@@ -369,7 +370,7 @@ describe('Type System: A Schema must have Object root types', () => {
       `),
     );
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Query root type must be Object type, it cannot be SomeInputObject.',
@@ -394,7 +395,7 @@ describe('Type System: A Schema must have Object root types', () => {
       // @ts-expect-error
       types: [{ name: 'SomeType' }, SomeDirective],
     });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Expected GraphQL named type but got: { name: "SomeType" }.',
       },
@@ -411,7 +412,7 @@ describe('Type System: A Schema must have Object root types', () => {
       // @ts-expect-error
       directives: [null, 'SomeDirective', SomeScalarType],
     });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Expected directive but got: null.',
       },
@@ -437,7 +438,7 @@ describe('Type System: Objects must have fields', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Object type with missing fields', () => {
@@ -448,7 +449,7 @@ describe('Type System: Objects must have fields', () => {
 
       type IncompleteObject
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Type IncompleteObject must define one or more fields.',
         locations: [{ line: 6, column: 7 }],
@@ -461,7 +462,7 @@ describe('Type System: Objects must have fields', () => {
         fields: {},
       }),
     );
-    expect(validateSchema(manualSchema)).to.deep.equal([
+    expectJSON(validateSchema(manualSchema)).to.deep.equal([
       {
         message: 'Type IncompleteObject must define one or more fields.',
       },
@@ -475,7 +476,7 @@ describe('Type System: Objects must have fields', () => {
         },
       }),
     );
-    expect(validateSchema(manualSchema2)).to.deep.equal([
+    expectJSON(validateSchema(manualSchema2)).to.deep.equal([
       {
         message: 'Type IncompleteObject must define one or more fields.',
       },
@@ -489,7 +490,7 @@ describe('Type System: Objects must have fields', () => {
         fields: { 'bad-name-with-dashes': { type: GraphQLString } },
       }),
     );
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.',
@@ -513,7 +514,7 @@ describe('Type System: Fields args must be properly named', () => {
         },
       }),
     );
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects field arg with invalid names', () => {
@@ -531,7 +532,7 @@ describe('Type System: Fields args must be properly named', () => {
       }),
     );
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.',
@@ -559,7 +560,7 @@ describe('Type System: Union types must be valid', () => {
         | TypeA
         | TypeB
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects a Union type with empty types', () => {
@@ -580,7 +581,7 @@ describe('Type System: Union types must be valid', () => {
       `),
     );
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Union type BadUnion must define one or more member types.',
         locations: [
@@ -611,7 +612,7 @@ describe('Type System: Union types must be valid', () => {
         | TypeA
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Union type BadUnion can only include type TypeA once.',
         locations: [
@@ -623,7 +624,7 @@ describe('Type System: Union types must be valid', () => {
 
     schema = extendSchema(schema, parse('extend union BadUnion = TypeB'));
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Union type BadUnion can only include type TypeA once.',
         locations: [
@@ -663,7 +664,7 @@ describe('Type System: Union types must be valid', () => {
 
     schema = extendSchema(schema, parse('extend union BadUnion = Int'));
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Union type BadUnion can only include Object types, it cannot include String.',
@@ -692,7 +693,7 @@ describe('Type System: Union types must be valid', () => {
         types: [memberType],
       });
       const badSchema = schemaWithFieldType(badUnion);
-      expect(validateSchema(badSchema)).to.deep.equal([
+      expectJSON(validateSchema(badSchema)).to.deep.equal([
         {
           message:
             'Union type BadUnion can only include Object types, ' +
@@ -714,7 +715,7 @@ describe('Type System: Input Objects must have fields', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Input Object type with missing fields', () => {
@@ -735,7 +736,7 @@ describe('Type System: Input Objects must have fields', () => {
       `),
     );
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Input Object type SomeInputObject must define one or more fields.',
@@ -766,7 +767,7 @@ describe('Type System: Input Objects must have fields', () => {
       }
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Input Object with non-breakable circular reference', () => {
@@ -780,7 +781,7 @@ describe('Type System: Input Objects must have fields', () => {
       }
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "nonNullSelf".',
@@ -808,7 +809,7 @@ describe('Type System: Input Objects must have fields', () => {
       }
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.nextInLoop.closeLoop".',
@@ -842,7 +843,7 @@ describe('Type System: Input Objects must have fields', () => {
       }
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.closeLoop".',
@@ -885,7 +886,7 @@ describe('Type System: Input Objects must have fields', () => {
         goodInputObject: SomeInputObject
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of SomeInputObject.badObject must be Input Type but got: SomeObject.',
@@ -911,7 +912,7 @@ describe('Type System: Input Objects must have fields', () => {
         anotherOptionalField: String! = "" @deprecated
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Required input field SomeInputObject.badField cannot be deprecated.',
@@ -943,7 +944,7 @@ describe('Type System: Enum types must be well defined', () => {
       `),
     );
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Enum type SomeEnum must define one or more values.',
         locations: [
@@ -965,7 +966,7 @@ describe('Type System: Enum types must be well defined', () => {
     }
 
     const schema1 = schemaWithEnum({ '#value': {} });
-    expect(validateSchema(schema1)).to.deep.equal([
+    expectJSON(validateSchema(schema1)).to.deep.equal([
       {
         message:
           'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.',
@@ -973,7 +974,7 @@ describe('Type System: Enum types must be well defined', () => {
     ]);
 
     const schema2 = schemaWithEnum({ '1value': {} });
-    expect(validateSchema(schema2)).to.deep.equal([
+    expectJSON(validateSchema(schema2)).to.deep.equal([
       {
         message:
           'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "1value" does not.',
@@ -981,7 +982,7 @@ describe('Type System: Enum types must be well defined', () => {
     ]);
 
     const schema3 = schemaWithEnum({ 'KEBAB-CASE': {} });
-    expect(validateSchema(schema3)).to.deep.equal([
+    expectJSON(validateSchema(schema3)).to.deep.equal([
       {
         message:
           'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "KEBAB-CASE" does not.',
@@ -989,17 +990,17 @@ describe('Type System: Enum types must be well defined', () => {
     ]);
 
     const schema4 = schemaWithEnum({ true: {} });
-    expect(validateSchema(schema4)).to.deep.equal([
+    expectJSON(validateSchema(schema4)).to.deep.equal([
       { message: 'Enum type SomeEnum cannot include value: true.' },
     ]);
 
     const schema5 = schemaWithEnum({ false: {} });
-    expect(validateSchema(schema5)).to.deep.equal([
+    expectJSON(validateSchema(schema5)).to.deep.equal([
       { message: 'Enum type SomeEnum cannot include value: false.' },
     ]);
 
     const schema6 = schemaWithEnum({ null: {} });
-    expect(validateSchema(schema6)).to.deep.equal([
+    expectJSON(validateSchema(schema6)).to.deep.equal([
       { message: 'Enum type SomeEnum cannot include value: null.' },
     ]);
   });
@@ -1031,14 +1032,14 @@ describe('Type System: Object fields must have output types', () => {
     const typeName = inspect(type);
     it(`accepts an output type as an Object field type: ${typeName}`, () => {
       const schema = schemaWithObjectField({ type });
-      expect(validateSchema(schema)).to.deep.equal([]);
+      expectJSON(validateSchema(schema)).to.deep.equal([]);
     });
   }
 
   it('rejects an empty Object field type', () => {
     // @ts-expect-error (type field must not be undefined)
     const schema = schemaWithObjectField({ type: undefined });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of BadObject.badField must be Output Type but got: undefined.',
@@ -1051,7 +1052,7 @@ describe('Type System: Object fields must have output types', () => {
     it(`rejects a non-output type as an Object field type: ${typeStr}`, () => {
       // @ts-expect-error
       const schema = schemaWithObjectField({ type });
-      expect(validateSchema(schema)).to.deep.equal([
+      expectJSON(validateSchema(schema)).to.deep.equal([
         {
           message: `The type of BadObject.badField must be Output Type but got: ${typeStr}.`,
         },
@@ -1062,7 +1063,7 @@ describe('Type System: Object fields must have output types', () => {
   it('rejects a non-type value as an Object field type', () => {
     // @ts-expect-error
     const schema = schemaWithObjectField({ type: Number });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of BadObject.badField must be Output Type but got: [function Number].',
@@ -1083,7 +1084,7 @@ describe('Type System: Object fields must have output types', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of Query.field must be Output Type but got: [SomeInputObject].',
@@ -1104,7 +1105,7 @@ describe('Type System: Objects can only implement unique interfaces', () => {
       }),
     });
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type BadObject must only implement Interface types, it cannot implement undefined.',
@@ -1126,7 +1127,7 @@ describe('Type System: Objects can only implement unique interfaces', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type BadObject must only implement Interface types, it cannot implement SomeInputObject.',
@@ -1149,7 +1150,7 @@ describe('Type System: Objects can only implement unique interfaces', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message: 'Type AnotherObject can only implement AnotherInterface once.',
         locations: [
@@ -1178,7 +1179,7 @@ describe('Type System: Objects can only implement unique interfaces', () => {
       schema,
       parse('extend type AnotherObject implements AnotherInterface'),
     );
-    expect(validateSchema(extendedSchema)).to.deep.equal([
+    expectJSON(validateSchema(extendedSchema)).to.deep.equal([
       {
         message: 'Type AnotherObject can only implement AnotherInterface once.',
         locations: [
@@ -1217,7 +1218,7 @@ describe('Type System: Interface extensions should be valid', () => {
         }
       `),
     );
-    expect(validateSchema(extendedSchema)).to.deep.equal([
+    expectJSON(validateSchema(extendedSchema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.newField expected but AnotherObject does not provide it.',
@@ -1256,7 +1257,7 @@ describe('Type System: Interface extensions should be valid', () => {
         }
       `),
     );
-    expect(validateSchema(extendedSchema)).to.deep.equal([
+    expectJSON(validateSchema(extendedSchema)).to.deep.equal([
       {
         message:
           'Interface field argument AnotherInterface.newField(test:) expected but AnotherObject.newField does not provide it.',
@@ -1307,7 +1308,7 @@ describe('Type System: Interface extensions should be valid', () => {
         }
       `),
     );
-    expect(validateSchema(extendedSchema)).to.deep.equal([
+    expectJSON(validateSchema(extendedSchema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.newInterfaceField expects type NewInterface but AnotherObject.newInterfaceField is type MismatchingInterface.',
@@ -1352,14 +1353,14 @@ describe('Type System: Interface fields must have output types', () => {
     const typeName = inspect(type);
     it(`accepts an output type as an Interface field type: ${typeName}`, () => {
       const schema = schemaWithInterfaceField({ type });
-      expect(validateSchema(schema)).to.deep.equal([]);
+      expectJSON(validateSchema(schema)).to.deep.equal([]);
     });
   }
 
   it('rejects an empty Interface field type', () => {
     // @ts-expect-error (type field must not be undefined)
     const schema = schemaWithInterfaceField({ type: undefined });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of BadImplementing.badField must be Output Type but got: undefined.',
@@ -1376,7 +1377,7 @@ describe('Type System: Interface fields must have output types', () => {
     it(`rejects a non-output type as an Interface field type: ${typeStr}`, () => {
       // @ts-expect-error
       const schema = schemaWithInterfaceField({ type });
-      expect(validateSchema(schema)).to.deep.equal([
+      expectJSON(validateSchema(schema)).to.deep.equal([
         {
           message: `The type of BadImplementing.badField must be Output Type but got: ${typeStr}.`,
         },
@@ -1390,7 +1391,7 @@ describe('Type System: Interface fields must have output types', () => {
   it('rejects a non-type value as an Interface field type', () => {
     // @ts-expect-error
     const schema = schemaWithInterfaceField({ type: Number });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of BadImplementing.badField must be Output Type but got: [function Number].',
@@ -1423,7 +1424,7 @@ describe('Type System: Interface fields must have output types', () => {
         field: SomeInputObject
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of SomeInterface.field must be Output Type but got: SomeInputObject.',
@@ -1447,7 +1448,7 @@ describe('Type System: Interface fields must have output types', () => {
         foo: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 });
 
@@ -1488,14 +1489,14 @@ describe('Type System: Arguments must have input types', () => {
     const typeName = inspect(type);
     it(`accepts an input type as a field arg type: ${typeName}`, () => {
       const schema = schemaWithArg({ type });
-      expect(validateSchema(schema)).to.deep.equal([]);
+      expectJSON(validateSchema(schema)).to.deep.equal([]);
     });
   }
 
   it('rejects an empty field arg type', () => {
     // @ts-expect-error (type field must not be undefined)
     const schema = schemaWithArg({ type: undefined });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of @BadDirective(badArg:) must be Input Type but got: undefined.',
@@ -1512,7 +1513,7 @@ describe('Type System: Arguments must have input types', () => {
     it(`rejects a non-input type as a field arg type: ${typeStr}`, () => {
       // @ts-expect-error
       const schema = schemaWithArg({ type });
-      expect(validateSchema(schema)).to.deep.equal([
+      expectJSON(validateSchema(schema)).to.deep.equal([
         {
           message: `The type of @BadDirective(badArg:) must be Input Type but got: ${typeStr}.`,
         },
@@ -1526,7 +1527,7 @@ describe('Type System: Arguments must have input types', () => {
   it('rejects a non-type value as a field arg type', () => {
     // @ts-expect-error
     const schema = schemaWithArg({ type: Number });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of @BadDirective(badArg:) must be Input Type but got: [function Number].',
@@ -1557,7 +1558,7 @@ describe('Type System: Arguments must have input types', () => {
         ): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Required argument @BadDirective(badArg:) cannot be deprecated.',
@@ -1586,7 +1587,7 @@ describe('Type System: Arguments must have input types', () => {
         foo: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of Query.test(arg:) must be Input Type but got: SomeObject.',
@@ -1626,14 +1627,14 @@ describe('Type System: Input Object fields must have input types', () => {
     const typeName = inspect(type);
     it(`accepts an input type as an input field type: ${typeName}`, () => {
       const schema = schemaWithInputField({ type });
-      expect(validateSchema(schema)).to.deep.equal([]);
+      expectJSON(validateSchema(schema)).to.deep.equal([]);
     });
   }
 
   it('rejects an empty input field type', () => {
     // @ts-expect-error (type field must not be undefined)
     const schema = schemaWithInputField({ type: undefined });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of BadInputObject.badField must be Input Type but got: undefined.',
@@ -1646,7 +1647,7 @@ describe('Type System: Input Object fields must have input types', () => {
     it(`rejects a non-input type as an input field type: ${typeStr}`, () => {
       // @ts-expect-error
       const schema = schemaWithInputField({ type });
-      expect(validateSchema(schema)).to.deep.equal([
+      expectJSON(validateSchema(schema)).to.deep.equal([
         {
           message: `The type of BadInputObject.badField must be Input Type but got: ${typeStr}.`,
         },
@@ -1657,7 +1658,7 @@ describe('Type System: Input Object fields must have input types', () => {
   it('rejects a non-type value as an input field type', () => {
     // @ts-expect-error
     const schema = schemaWithInputField({ type: Number });
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of BadInputObject.badField must be Input Type but got: [function Number].',
@@ -1682,7 +1683,7 @@ describe('Type System: Input Object fields must have input types', () => {
         bar: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'The type of SomeInputObject.foo must be Input Type but got: SomeObject.',
@@ -1707,7 +1708,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field(input: String): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('accepts an Object which implements an Interface along with more fields', () => {
@@ -1725,7 +1726,7 @@ describe('Objects must adhere to Interface they implement', () => {
         anotherField: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('accepts an Object which implements an Interface field along with additional optional arguments', () => {
@@ -1742,7 +1743,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field(input: String, anotherInput: String): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Object missing an Interface field', () => {
@@ -1759,7 +1760,7 @@ describe('Objects must adhere to Interface they implement', () => {
         anotherField: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expected but AnotherObject does not provide it.',
@@ -1785,7 +1786,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field(input: String): Int
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expects type String but AnotherObject.field is type Int.',
@@ -1814,7 +1815,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: B
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expects type A but AnotherObject.field is type B.',
@@ -1840,7 +1841,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: AnotherObject
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('accepts an Object with a subtyped Interface field (union)', () => {
@@ -1863,7 +1864,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: SomeObject
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Object missing an Interface argument', () => {
@@ -1880,7 +1881,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field argument AnotherInterface.field(input:) expected but AnotherObject.field does not provide it.',
@@ -1906,7 +1907,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field(input: Int): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field argument AnotherInterface.field(input:) expects type String but AnotherObject.field(input:) is type Int.',
@@ -1932,7 +1933,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field(input: Int): Int
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expects type String but AnotherObject.field is type Int.',
@@ -1971,7 +1972,7 @@ describe('Objects must adhere to Interface they implement', () => {
         ): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Object field AnotherObject.field includes required argument requiredArg that is missing from the Interface field AnotherInterface.field.',
@@ -1997,7 +1998,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: [String]!
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Object with a non-list Interface field list type', () => {
@@ -2014,7 +2015,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expects type [String] but AnotherObject.field is type String.',
@@ -2040,7 +2041,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: [String]
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expects type String but AnotherObject.field is type [String].',
@@ -2066,7 +2067,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: String!
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Object with a superset nullable Interface field type', () => {
@@ -2083,7 +2084,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field AnotherInterface.field expects type String! but AnotherObject.field is type String.',
@@ -2113,7 +2114,7 @@ describe('Objects must adhere to Interface they implement', () => {
         field: String!
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type AnotherObject must implement SuperInterface because it is implemented by AnotherInterface.',
@@ -2141,7 +2142,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field(input: String): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('accepts an Interface which implements an Interface along with more fields', () => {
@@ -2159,7 +2160,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         anotherField: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('accepts an Interface which implements an Interface field along with additional optional arguments', () => {
@@ -2176,7 +2177,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field(input: String, anotherInput: String): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Interface missing an Interface field', () => {
@@ -2193,7 +2194,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         anotherField: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expected but ChildInterface does not provide it.',
@@ -2219,7 +2220,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field(input: String): Int
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expects type String but ChildInterface.field is type Int.',
@@ -2248,7 +2249,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: B
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expects type A but ChildInterface.field is type B.',
@@ -2274,7 +2275,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: ChildInterface
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('accepts an Interface with a subtyped Interface field (union)', () => {
@@ -2297,7 +2298,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: SomeObject
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Interface implementing a non-Interface type', () => {
@@ -2314,7 +2315,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type BadInterface must only implement Interface types, it cannot implement SomeInputObject.',
@@ -2337,7 +2338,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field argument ParentInterface.field(input:) expected but ChildInterface.field does not provide it.',
@@ -2363,7 +2364,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field(input: Int): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field argument ParentInterface.field(input:) expects type String but ChildInterface.field(input:) is type Int.',
@@ -2389,7 +2390,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field(input: Int): Int
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expects type String but ChildInterface.field is type Int.',
@@ -2428,7 +2429,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         ): String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Object field ChildInterface.field includes required argument requiredArg that is missing from the Interface field ParentInterface.field.',
@@ -2454,7 +2455,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: [String]!
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Interface with a non-list Interface field list type', () => {
@@ -2471,7 +2472,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expects type [String] but ChildInterface.field is type String.',
@@ -2497,7 +2498,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: [String]
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expects type String but ChildInterface.field is type [String].',
@@ -2523,7 +2524,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: String!
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([]);
+    expectJSON(validateSchema(schema)).to.deep.equal([]);
   });
 
   it('rejects an Interface with a superset nullable Interface field type', () => {
@@ -2540,7 +2541,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: String
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Interface field ParentInterface.field expects type String! but ChildInterface.field is type String.',
@@ -2570,7 +2571,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
         field: String!
       }
     `);
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type ChildInterface must implement SuperInterface because it is implemented by ParentInterface.',
@@ -2593,7 +2594,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
       }
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type FooInterface cannot implement itself because it would create a circular reference.',
@@ -2617,7 +2618,7 @@ describe('Interfaces must adhere to Interface they implement', () => {
       }
     `);
 
-    expect(validateSchema(schema)).to.deep.equal([
+    expectJSON(validateSchema(schema)).to.deep.equal([
       {
         message:
           'Type FooInterface cannot implement BarInterface because it would create a circular reference.',
diff --git a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts
index 1f3cbab8..e14b0f35 100644
--- a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts
+++ b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts
@@ -1,11 +1,17 @@
+import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { inspect } from '../../jsutils/inspect';
 
+import { parse } from '../../language/parser';
+
 import { GraphQLSchema } from '../../type/schema';
 import { GraphQLString } from '../../type/scalars';
 import { GraphQLScalarType, GraphQLObjectType } from '../../type/definition';
 
+import { validate } from '../validate';
 import { ValuesOfCorrectTypeRule } from '../rules/ValuesOfCorrectTypeRule';
 
 import {
@@ -962,12 +968,10 @@ describe('Validate: Values of correct type', () => {
         }),
       });
 
-      const expectedErrors = expectErrorsWithSchema(
-        schema,
-        '{ invalidArg(arg: 123) }',
-      );
+      const doc = parse('{ invalidArg(arg: 123) }');
+      const errors = validate(schema, doc, [ValuesOfCorrectTypeRule]);
 
-      expectedErrors.to.deep.equal([
+      expectJSON(errors).to.deep.equal([
         {
           message:
             'Expected value of type "Invalid", found 123; Invalid scalar is always invalid: 123',
@@ -975,8 +979,8 @@ describe('Validate: Values of correct type', () => {
         },
       ]);
 
-      expectedErrors.to.have.nested.property(
-        '[0].originalError.message',
+      expect(errors[0]).to.have.nested.property(
+        'originalError.message',
         'Invalid scalar is always invalid: 123',
       );
     });
diff --git a/src/validation/__tests__/harness.ts b/src/validation/__tests__/harness.ts
index 139827af..304cde67 100644
--- a/src/validation/__tests__/harness.ts
+++ b/src/validation/__tests__/harness.ts
@@ -1,4 +1,4 @@
-import { expect } from 'chai';
+import { expectJSON } from '../../__testUtils__/expectJSON';
 
 import type { Maybe } from '../../jsutils/Maybe';
 
@@ -122,7 +122,7 @@ export function expectValidationErrorsWithSchema(
 ): any {
   const doc = parse(queryStr);
   const errors = validate(schema, doc, [rule]);
-  return expect(errors);
+  return expectJSON(errors);
 }
 
 export function expectValidationErrors(
@@ -139,5 +139,5 @@ export function expectSDLValidationErrors(
 ): any {
   const doc = parse(sdlStr);
   const errors = validateSDL(doc, schema, [rule]);
-  return expect(errors);
+  return expectJSON(errors);
 }
diff --git a/src/validation/__tests__/validation-test.ts b/src/validation/__tests__/validation-test.ts
index af850891..65a85613 100644
--- a/src/validation/__tests__/validation-test.ts
+++ b/src/validation/__tests__/validation-test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
+import { expectJSON } from '../../__testUtils__/expectJSON';
+
 import { GraphQLError } from '../../error/GraphQLError';
 
 import type { DirectiveNode } from '../../language/ast';
@@ -37,7 +39,7 @@ describe('Validate: Supports full validation', () => {
     `);
 
     const errors = validate(testSchema, doc);
-    expect(errors).to.deep.equal([]);
+    expectJSON(errors).to.deep.equal([]);
   });
 
   it('detects unknown fields', () => {
@@ -48,7 +50,7 @@ describe('Validate: Supports full validation', () => {
     `);
 
     const errors = validate(testSchema, doc);
-    expect(errors).to.deep.equal([
+    expectJSON(errors).to.deep.equal([
       {
         locations: [{ line: 3, column: 9 }],
         message: 'Cannot query field "unknown" on type "QueryRoot".',
@@ -114,7 +116,7 @@ describe('Validate: Supports full validation', () => {
     }
 
     const errors = validate(schema, doc, [customRule]);
-    expect(errors).to.deep.equal([
+    expectJSON(errors).to.deep.equal([
       {
         message: 'Reporting directive: @custom',
         locations: [{ line: 3, column: 14 }],
@@ -146,7 +148,7 @@ describe('Validate: Limit maximum number of validation errors', () => {
 
   it('when maxErrors is equal to number of errors', () => {
     const errors = validateDocument({ maxErrors: 3 });
-    expect(errors).to.be.deep.equal([
+    expectJSON(errors).to.be.deep.equal([
       invalidFieldError('firstUnknownField'),
       invalidFieldError('secondUnknownField'),
       invalidFieldError('thirdUnknownField'),
@@ -155,7 +157,7 @@ describe('Validate: Limit maximum number of validation errors', () => {
 
   it('when maxErrors is less than number of errors', () => {
     const errors = validateDocument({ maxErrors: 2 });
-    expect(errors).to.be.deep.equal([
+    expectJSON(errors).to.be.deep.equal([
       invalidFieldError('firstUnknownField'),
       invalidFieldError('secondUnknownField'),
       {