🎉 Explore GraphQLConf 2026 • May 19-21 • Fremont, CA • View the schedule
Upgrade Guidesv14 to v15

What Changed in GraphQL.js v15

GraphQL.js v15 is an older release line. GraphQL.js v16 is the current stable release line, and a v17 release candidate is available for final testing and feedback. If you are upgrading from v14, use this guide first, then continue through v15 to v16 and v16 to v17.

GraphQL.js v15 is mainly a compatibility cleanup and SDL modernization release. It keeps the core v14 request flow: parse, validate, execute, and use the schema types from graphql/type. Most migration work is around removed deprecated utilities, newer schema-language features, and stricter enum and scalar coercion behavior. Complete this step before applying later major-version guides so failures stay tied to one version boundary.

Contents

Using this Guide

This guide is for projects that depend on GraphQL.js, including applications, servers, libraries, and tools. It compares the latest available v14 and v15 release lines, and focuses on differences that still exist between those lines. If a change first appeared during the v15 line but also shipped in the latest v14 line, it is not listed here as v14-to-v15 migration work.

Changes described below use these labels:

  • Breaking change: v14 code may need to change before it runs on v15.
  • Behavioral tightening: v15 validates, coerces, or reports a case more precisely.
  • Deprecation: the v14 API still works in v15, but should be migrated before v16.
  • New stable API: a new public API that can be adopted independently.
  • Experimental or opt-in: available in v15, but proposal-backed or outside the default request path.

Platform and Package Shape

Node.js and package dependencies

Breaking change. GraphQL.js v15 requires Node.js 10 and later:

{
  "engines": {
    "node": ">= 10.x"
  }
}

Upgrade Node.js before upgrading GraphQL.js. This separates runtime and package-manager errors from GraphQL.js migration errors.

Breaking change. GraphQL.js v15 no longer depends on its old iterator helper package or babel-polyfill. Subscription and async-iterator code uses native iterator protocols. If a browser or non-Node runtime does not provide the iterator features your application needs, provide those polyfills in the host application.

TypeScript definitions

Breaking change. The published TypeScript definitions were reorganized, but public imports from graphql, graphql/language, graphql/type, graphql/execution, graphql/subscription, graphql/utilities, and graphql/validation continue to work.

GraphQL.js v15 also removes several TypeScript-only generic parameters that did not exist in the JavaScript runtime surface, including the extra data, source, and args type parameters on common execution and field types. Let those types infer from the remaining public generics instead of passing removed parameters.

Removed Utilities and Renamed Entry Points

Input coercion helpers

Breaking change. The deprecated coerceValue() helper was removed. Use coerceInputValue() for JavaScript input values.

- import { coerceValue } from 'graphql/utilities';
+ import { coerceInputValue } from 'graphql/utilities';
 
- const result = coerceValue(value, inputType);
+ const result = coerceInputValue(value, inputType);

coerceInputValue() reports errors through an optional callback. If the callback is omitted, it throws on the first invalid value.

Breaking change. isValidJSValue() and isValidLiteralValue() were removed. Use coerceInputValue() when you need to validate a JavaScript input value, and use validate() plus the specified validation rules when you need to validate a document.

Introspection query helper

Breaking change. The deprecated introspectionQuery string constant was removed. Use getIntrospectionQuery() so callers can request optional introspection fields supported by the target server.

- import { introspectionQuery } from 'graphql';
+ import { getIntrospectionQuery } from 'graphql';
 
- const source = introspectionQuery;
+ const source = getIntrospectionQuery();

Lexer construction

Breaking change. createLexer() was removed. Use the Lexer class with a Source instance.

- import { createLexer, Source } from 'graphql/language';
+ import { Lexer, Source } from 'graphql/language';
 
  const source = new Source('{ field }');
- const lexer = createLexer(source);
+ const lexer = new Lexer(source);

Lexer, Location, and Token are public classes in v15. Code that only uses parse() does not need to construct them directly.

File-level imports

Breaking change for file-level imports. GraphQL.js only treats the root package and package-module entry points as semver-stable API boundaries. Prefer imports from graphql, graphql/language, graphql/type, graphql/execution, graphql/subscription, graphql/utilities, and graphql/validation.

If you import individual files, v15 changes a few file names and export names. The most common cases are summarized in Deep Import Moves.

Schema Construction and SDL

Schema extensions in buildSchema()

New stable API. buildSchema() accepts documents that contain type, interface, union, enum, input object, scalar, directive, and schema extensions. In v14, buildSchema() ignored extension definitions and only the original definition contributed fields.

type Query {
  a: String
}
 
extend type Query {
  b: String
}

In v15, the resulting Query type has both a and b. If you had custom SDL preprocessing that separated definitions from extensions before calling buildSchema(), simplify it and let GraphQL.js apply the extensions.

Interfaces implementing interfaces

New stable API. SDL and code-first schemas can model interfaces that implement other interfaces.

interface Node {
  id: ID
}
 
interface Resource implements Node {
  id: ID
}

Run validateSchema() after migration. Object types implementing an interface inherit the same field subtype obligations that apply to directly implemented interfaces.

Schema descriptions

New stable API. Schema definitions can have descriptions.

"Public API schema"
schema {
  query: Query
}

GraphQLSchema also exposes description. Code that serializes schema metadata should decide whether to preserve or display the new description field.

Custom scalar specification URLs

New stable API. v15 supports the @specifiedBy directive and the programmatic specifiedByUrl scalar configuration field.

const URLScalar = new GraphQLScalarType({
  name: 'URL',
  specifiedByUrl: 'https://example.com/url-spec',
});

printSchema() emits @specifiedBy(url: ...), buildSchema() reads it, and introspection can include specifiedByUrl when requested with getIntrospectionQuery({ specifiedByUrl: true }).

Deprecating input values

New stable API. v15 supports @deprecated on field arguments, directive arguments, and input object fields.

input SearchInput {
  oldField: String @deprecated(reason: "Use newField")
  newField: String
}
 
type Query {
  search(oldArg: String @deprecated(reason: "Use input")): String
}

Introspection fields such as args and inputFields accept includeDeprecated. Clients and tooling that assume those lists contain only currently recommended values should keep the default includeDeprecated: false.

Empty deprecation reasons

Behavioral tightening. Empty deprecation reasons count as deprecations. In v14, a field with deprecationReason: '' could appear as not deprecated. In v15, the field is deprecated and the empty reason is preserved.

If code checks field.isDeprecated, prefer checking field.deprecationReason != null so the same logic works with the v16 field shape too.

Coercion and Serialization

Enum values

Breaking change. GraphQLEnumType no longer preserves undefined as a custom internal enum value. In v14, { value: undefined } made parseValue() return undefined for that enum name and let serialize(undefined) return the enum name. In v15, value: undefined is treated like an omitted value, so the internal value becomes the enum name and serialize(undefined) is invalid.

  const Episode = new GraphQLEnumType({
    name: 'Episode',
    values: {
-     NEW_HOPE: { value: undefined },
+     NEW_HOPE: {},
    },
  });

Omit value when the runtime value should be the enum name. If you used undefined as a real sentinel value, replace it with an explicit runtime value before upgrading.

Behavioral tightening. GraphQLEnumType.serialize(), GraphQLEnumType.parseValue(), and GraphQLEnumType.parseLiteral() throw when the provided value does not match a known enum value. v14 returned undefined for several invalid values.

const Episode = new GraphQLEnumType({
  name: 'Episode',
  values: { NEW_HOPE: { value: 4 } },
});
 
Episode.serialize(5); // throws in v15

Built-in scalars

Behavioral tightening. Float execution now reports NaN serialization as a field error. GraphQLFloat.serialize(NaN) already threw in the latest v14 line, but returning NaN from a resolver for a Float field could still produce a null JSON value without a field error. In v15, the field reports the serialization error.

astFromValue(NaN, GraphQLFloat) also throws in v15.

New stable behavior. GraphQLInt, GraphQLFloat, and GraphQLBoolean serialize object wrappers through their primitive conversion hooks.

const value = { valueOf: () => 7 };
 
GraphQLInt.serialize(value); // 7 in v15

If your resolvers accidentally return boxed or object-wrapped scalar values, v15 may now accept them where v14 reported errors. Prefer returning primitive JavaScript values from resolvers for predictable behavior.

Execution and Validation

Request API object arguments

New stable API. v15 accepts object arguments for graphql(), graphqlSync(), execute(), executeSync(), and subscribe(). The v14 positional forms still work in v15, but the object forms are the v16-compatible request API shape.

- const result = await graphql(schema, source, rootValue, contextValue);
+ const result = await graphql({
+   schema,
+   source,
+   rootValue,
+   contextValue,
+ });

If you plan to continue to v16, convert these call sites while still on v15 so the mechanical API change is separate from the major-version bump.

Synchronous execution helper

New stable API. executeSync() is available from the root package and graphql/execution. Use it when every resolver is expected to complete synchronously and an asynchronous resolver should be treated as a programming error.

import { executeSync, parse } from 'graphql';
 
const result = executeSync({ schema, document: parse('{ viewer { id } }') });

Execution result and error formatting

New stable API. The TypeScript ExecutionResult type includes optional extensions. This matches GraphQL responses that carry out-of-band metadata such as tracing identifiers or cache hints.

New stable API. GraphQLError has toJSON(), and the package exports GraphQLFormattedError types for formatter code. Code that stringifies errors through JSON can rely on toJSON() rather than duplicating GraphQL error formatting logic.

Deprecated-usage validation

New stable API. NoDeprecatedCustomRule is available as a validation rule for operations that should reject deprecated fields, arguments, input fields, or enum values.

import { NoDeprecatedCustomRule, validate } from 'graphql';
 
const errors = validate(schema, document, [NoDeprecatedCustomRule]);

findDeprecatedUsages() still exists in v15, but a validation rule is easier to compose with the rest of the request pipeline.

New stable API. NoSchemaIntrospectionCustomRule rejects introspection fields in executable documents. Use it only for hosts that intentionally block introspection.

New stable API. The v15 line includes recommendedRules and MaxIntrospectionDepthRule. specifiedRules remains the GraphQL specification validation set. recommendedRules contains GraphQL.js-specific rules that many public servers should consider but that are not required by the specification.

import { recommendedRules, specifiedRules, validate } from 'graphql';
 
const errors = validate(schema, document, [
  ...specifiedRules,
  ...recommendedRules,
]);

v15 Compatibility APIs to Clear Before v16

These APIs still work in v15, but v16 removes them or replaces them with different names or call shapes. Migrate them during the v15 step when possible:

  • Convert positional graphql(), graphqlSync(), execute(), executeSync(), and subscribe() calls to object arguments.
  • Replace findDeprecatedUsages() with validate() plus NoDeprecatedCustomRule.
  • Replace comment descriptions and commentDescriptions usage with string or block-string descriptions. Read description fields directly instead of calling getDescription().
  • Remove allowLegacySDLEmptyFields and allowLegacySDLImplementsInterfaces; use valid SDL instead.
  • Replace GraphQLSchema.isPossibleType() with schema.isSubType().
  • Replace field.isDeprecated and enumValue.isDeprecated reads with deprecationReason != null.
  • Plan to rename scalar specification URL config and introspection reads from specifiedByUrl to specifiedByURL when moving to v16.

Practical Migration Order

Use this order to keep the upgrade reviewable. The checklists below expand each category into concrete items.

  1. Update Node.js and remove runtime assumptions provided by old package dependencies.
  2. Replace removed utilities and lexer construction helpers.
  3. Update schema construction and SDL features, then run validateSchema().
  4. Update enum, scalar, and input coercion expectations.
  5. Clear v15 compatibility APIs that v16 removes.
  6. Adopt new validation rules and optional v15 schema features where they match your server design.

Run schema validation, operation validation, execution tests, and TypeScript checks after each group. Mechanical changes are easiest to review when they are separated from behavior changes.

Detailed Migration Checklists

Platform and package shape

  • Run v15 on Node.js 10 or newer.
  • Provide host-level iterator or browser polyfills if you support runtimes that do not have the native iterator features your application uses.
  • Update TypeScript code that references removed v14-only generic parameters.
  • Prefer public package entry points instead of files under the old top-level declaration tree.
  • If you import from individual files, review Deep Import Moves for moved or renamed file paths.

Removed APIs and required replacements

  • Replace coerceValue() with coerceInputValue().
  • Replace isValidJSValue() and isValidLiteralValue() with input coercion or document validation, depending on the source of the value.
  • Replace introspectionQuery with getIntrospectionQuery().
  • Replace createLexer(source, options) with new Lexer(source).
  • Remove allowedLegacyNames usage from GraphQLSchema construction.

Schema and SDL

  • Let buildSchema() consume extension definitions directly, or remove custom preprocessing that used to compensate for ignored extensions.
  • Validate schemas that use interface inheritance and make sure implementing object types satisfy inherited fields.
  • Preserve GraphQLSchema.description if your tooling serializes schema metadata.
  • Use specifiedByUrl or @specifiedBy for custom scalar specification URLs.
  • Decide how clients and tooling should handle deprecated arguments and input fields.
  • Treat deprecationReason: '' as a real deprecation.

v16 prep

  • Convert graphql(), graphqlSync(), execute(), executeSync(), and subscribe() positional calls to object arguments.
  • Replace findDeprecatedUsages() with validate() plus NoDeprecatedCustomRule.
  • Replace comment descriptions and commentDescriptions usage with string or block-string descriptions.
  • Remove allowLegacySDLEmptyFields and allowLegacySDLImplementsInterfaces usage.
  • Replace GraphQLSchema.isPossibleType() with schema.isSubType().
  • Replace field.isDeprecated and enumValue.isDeprecated reads with deprecationReason != null.
  • Rename scalar specification URL config and introspection reads from specifiedByUrl to specifiedByURL when moving to v16.

Coercion and execution

  • Remove value: undefined from enum value configs.
  • Update tests that expected enum serialize(), parseValue(), or parseLiteral() to return undefined for invalid values.
  • Update Float execution or astFromValue() tests that expected NaN to become null without an error.
  • Update scalar wrapper tests if you depended on v14 rejecting boxed or object-wrapped primitive values.
  • Use executeSync() for intentionally synchronous execution paths.
  • Preserve ExecutionResult.extensions in TypeScript helpers that wrap execution results.

Optional v15 features

  • Use GraphQLError.toJSON() and GraphQLFormattedError types for formatted error output.
  • Use NoDeprecatedCustomRule when deprecated schema elements should fail validation.
  • Use NoSchemaIntrospectionCustomRule only for hosts that intentionally block introspection.
  • Consider recommendedRules and MaxIntrospectionDepthRule for public servers.
  • Request specifiedByUrl in introspection only when the target server supports it.

Deep Import Moves

These notes apply only to file-level imports. Prefer package-module imports where possible because file-level paths are not semver-stable API boundaries.

  • graphql/utilities/schemaPrinter moved to graphql/utilities/printSchema. The public function names did not change: printSchema(), printType(), and printIntrospectionSchema() keep their names.
  • The getIntrospectionQuery() file-level import moved from graphql/utilities/introspectionQuery to graphql/utilities/getIntrospectionQuery. The function name did not change; the old introspectionQuery string constant did not move and should be replaced as described above.
  • Validation rule files were renamed to match the public *Rule export convention. For example, graphql/validation/rules/FieldsOnCorrectType became graphql/validation/rules/FieldsOnCorrectTypeRule, and the exported rule is FieldsOnCorrectTypeRule. The same convention applies to the v14 specified validation rules such as ValuesOfCorrectType, NoUnusedVariables, and ProvidedRequiredArguments.
  • The type-only helper graphql/tsutils/Maybe moved to graphql/jsutils/Maybe, with a named Maybe type export instead of the old default type export.