Repository: graphql/graphql-js (ts)

Issue:
Default value declared via SDL may not introspect correctly
> One interesting aspect here is that a default value declared via SDL is coerced twice:
> Once via `valueFromAST` and then again via `coerceInputValueImpl`. Is my reading here correct?
>
> I am not really sure this is a problem, but I wanted to call that out.
>
> _Originally posted by @andimarek in https://github.com/graphql/graphql-js/issues/3049#issuecomment-827036666_

That's a good call out. Coercing more than once should be safe, assuming that `coerce(X) == coerce(serialize(coerce(X)))` 

However I think this is likely exposing a real (but maybe low-impact) bug, even before this diff. Default values are exposed via introspection and are not expected to include nested default values:

```graphql
input A {
  a: B = {}
}

input B {
  c: Int = 0
}
```

When introspecting `A.a`, we should see `"defaultValue": "{}"` but if I read correctly likely will see `"defaultValue": "{c:0}"`

If you were to re-print this SDL after introspection, you'd have:

```graphql
input A {
  a: B = { c: 0 }
}
```

which broke the original intent.

Executed results would be correct, however schema representation would be off.

Notably, constructing the schema programmatically would not encounter this bug - only using AST creation would because of how `valueFromAST` works.

Pull request:
Preserve defaultValue literals
[#3074 rebased on main](https://github.com/graphql/graphql-js/pull/3074).

Depends on #3809

@leebyron comments from original PR (edited, hopefully correctly):

> Fixes #3051
> 
> This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either (EDIT: but not both!) "value" and "literal" fields.
> 
> Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL:
> 
> **Before this change:**
> 
> ```
> (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL)
> ```
> 
> `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in #3049.
> 
> **After this change:**
> 
> ```
> (SDL) --parse-> (defaultValue literal config) --print --> (SDL)
> ```

Patch:
diff --git a/src/execution/getVariableSignature.ts b/src/execution/getVariableSignature.ts
index d23c310e..8f9fd4a5 100644
--- a/src/execution/getVariableSignature.ts
+++ b/src/execution/getVariableSignature.ts
@@ -4,9 +4,12 @@ import type { VariableDefinitionNode } from '../language/ast.js';
 import { print } from '../language/printer.js';
 
 import { isInputType } from '../type/definition.js';
-import type { GraphQLInputType, GraphQLSchema } from '../type/index.js';
+import type {
+  GraphQLDefaultValueUsage,
+  GraphQLInputType,
+  GraphQLSchema,
+} from '../type/index.js';
 
-import { coerceInputLiteral } from '../utilities/coerceInputValue.js';
 import { typeFromAST } from '../utilities/typeFromAST.js';
 
 /**
@@ -18,7 +21,7 @@ import { typeFromAST } from '../utilities/typeFromAST.js';
 export interface GraphQLVariableSignature {
   name: string;
   type: GraphQLInputType;
-  defaultValue: unknown;
+  defaultValue: GraphQLDefaultValueUsage | undefined;
 }
 
 export function getVariableSignature(
@@ -43,8 +46,6 @@ export function getVariableSignature(
   return {
     name: varName,
     type: varType,
-    defaultValue: defaultValue
-      ? coerceInputLiteral(varDefNode.defaultValue, varType)
-      : undefined,
+    defaultValue: defaultValue ? { literal: defaultValue } : undefined,
   };
 }
diff --git a/src/execution/values.ts b/src/execution/values.ts
index c153a616..5ac0c765 100644
--- a/src/execution/values.ts
+++ b/src/execution/values.ts
@@ -20,6 +20,7 @@ import type { GraphQLDirective } from '../type/directives.js';
 import type { GraphQLSchema } from '../type/schema.js';
 
 import {
+  coerceDefaultValue,
   coerceInputLiteral,
   coerceInputValue,
 } from '../utilities/coerceInputValue.js';
@@ -90,8 +91,11 @@ function coerceVariableValues(
 
     const { name: varName, type: varType } = varSignature;
     if (!Object.hasOwn(inputs, varName)) {
-      if (varDefNode.defaultValue) {
-        coercedValues[varName] = varSignature.defaultValue;
+      if (varSignature.defaultValue) {
+        coercedValues[varName] = coerceDefaultValue(
+          varSignature.defaultValue,
+          varType,
+        );
       } else if (isNonNullType(varType)) {
         const varTypeStr = inspect(varType);
         onError(
@@ -173,8 +177,11 @@ export function experimentalGetArgumentValues(
     const argumentNode = argNodeMap.get(name);
 
     if (argumentNode == null) {
-      if (argDef.defaultValue !== undefined) {
-        coercedValues[name] = argDef.defaultValue;
+      if (argDef.defaultValue) {
+        coercedValues[name] = coerceDefaultValue(
+          argDef.defaultValue,
+          argDef.type,
+        );
       } else if (isNonNullType(argType)) {
         throw new GraphQLError(
           `Argument "${name}" of required type "${inspect(argType)}" ` +
@@ -197,8 +204,11 @@ export function experimentalGetArgumentValues(
         scopedVariableValues == null ||
         !Object.hasOwn(scopedVariableValues, variableName)
       ) {
-        if (argDef.defaultValue !== undefined) {
-          coercedValues[name] = argDef.defaultValue;
+        if (argDef.defaultValue) {
+          coercedValues[name] = coerceDefaultValue(
+            argDef.defaultValue,
+            argDef.type,
+          );
         } else if (isNonNullType(argType)) {
           throw new GraphQLError(
             `Argument "${name}" of required type "${inspect(argType)}" ` +
diff --git a/src/index.ts b/src/index.ts
index cffd892d..27ec4301 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -199,6 +199,7 @@ export type {
   GraphQLScalarSerializer,
   GraphQLScalarValueParser,
   GraphQLScalarLiteralParser,
+  GraphQLDefaultValueUsage,
 } from './type/index.js';
 
 // Parse and operate on GraphQL language source files.
diff --git a/src/type/definition.ts b/src/type/definition.ts
index b0e783b6..4ebb5ae9 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -16,6 +16,7 @@ import { toObjMap } from '../jsutils/toObjMap.js';
 import { GraphQLError } from '../error/GraphQLError.js';
 
 import type {
+  ConstValueNode,
   EnumTypeDefinitionNode,
   EnumTypeExtensionNode,
   EnumValueDefinitionNode,
@@ -799,7 +800,7 @@ export function defineArguments(
     name: assertName(argName),
     description: argConfig.description,
     type: argConfig.type,
-    defaultValue: argConfig.defaultValue,
+    defaultValue: defineDefaultValue(argName, argConfig),
     deprecationReason: argConfig.deprecationReason,
     extensions: toObjMap(argConfig.extensions),
     astNode: argConfig.astNode,
@@ -833,7 +834,8 @@ export function argsToArgsConfig(
     (arg) => ({
       description: arg.description,
       type: arg.type,
-      defaultValue: arg.defaultValue,
+      defaultValue: arg.defaultValue?.value,
+      defaultValueLiteral: arg.defaultValue?.literal,
       deprecationReason: arg.deprecationReason,
       extensions: arg.extensions,
       astNode: arg.astNode,
@@ -946,6 +948,7 @@ export interface GraphQLArgumentConfig {
   description?: Maybe<string>;
   type: GraphQLInputType;
   defaultValue?: unknown;
+  defaultValueLiteral?: ConstValueNode | undefined;
   deprecationReason?: Maybe<string>;
   extensions?: Maybe<Readonly<GraphQLArgumentExtensions>>;
   astNode?: Maybe<InputValueDefinitionNode>;
@@ -971,7 +974,7 @@ export interface GraphQLArgument {
   name: string;
   description: Maybe<string>;
   type: GraphQLInputType;
-  defaultValue: unknown;
+  defaultValue: GraphQLDefaultValueUsage | undefined;
   deprecationReason: Maybe<string>;
   extensions: Readonly<GraphQLArgumentExtensions>;
   astNode: Maybe<InputValueDefinitionNode>;
@@ -985,6 +988,26 @@ export type GraphQLFieldMap<TSource, TContext> = ObjMap<
   GraphQLField<TSource, TContext>
 >;
 
+export type GraphQLDefaultValueUsage =
+  | { value: unknown; literal?: never }
+  | { literal: ConstValueNode; value?: never };
+
+export function defineDefaultValue(
+  argName: string,
+  config: GraphQLArgumentConfig | GraphQLInputFieldConfig,
+): GraphQLDefaultValueUsage | undefined {
+  if (config.defaultValue === undefined && !config.defaultValueLiteral) {
+    return;
+  }
+  devAssert(
+    !(config.defaultValue !== undefined && config.defaultValueLiteral),
+    `Argument "${argName}" has both a defaultValue and a defaultValueLiteral property, but only one must be provided.`,
+  );
+  return config.defaultValueLiteral
+    ? { literal: config.defaultValueLiteral }
+    : { value: config.defaultValue };
+}
+
 /**
  * Custom extensions
  *
@@ -1538,7 +1561,8 @@ export class GraphQLInputObjectType {
     const fields = mapValue(this.getFields(), (field) => ({
       description: field.description,
       type: field.type,
-      defaultValue: field.defaultValue,
+      defaultValue: field.defaultValue?.value,
+      defaultValueLiteral: field.defaultValue?.literal,
       deprecationReason: field.deprecationReason,
       extensions: field.extensions,
       astNode: field.astNode,
@@ -1572,7 +1596,7 @@ function defineInputFieldMap(
     name: assertName(fieldName),
     description: fieldConfig.description,
     type: fieldConfig.type,
-    defaultValue: fieldConfig.defaultValue,
+    defaultValue: defineDefaultValue(fieldName, fieldConfig),
     deprecationReason: fieldConfig.deprecationReason,
     extensions: toObjMap(fieldConfig.extensions),
     astNode: fieldConfig.astNode,
@@ -1613,6 +1637,7 @@ export interface GraphQLInputFieldConfig {
   description?: Maybe<string>;
   type: GraphQLInputType;
   defaultValue?: unknown;
+  defaultValueLiteral?: ConstValueNode | undefined;
   deprecationReason?: Maybe<string>;
   extensions?: Maybe<Readonly<GraphQLInputFieldExtensions>>;
   astNode?: Maybe<InputValueDefinitionNode>;
@@ -1624,7 +1649,7 @@ export interface GraphQLInputField {
   name: string;
   description: Maybe<string>;
   type: GraphQLInputType;
-  defaultValue: unknown;
+  defaultValue: GraphQLDefaultValueUsage | undefined;
   deprecationReason: Maybe<string>;
   extensions: Readonly<GraphQLInputFieldExtensions>;
   astNode: Maybe<InputValueDefinitionNode>;
diff --git a/src/type/index.ts b/src/type/index.ts
index 6617a7bf..dfbca75a 100644
--- a/src/type/index.ts
+++ b/src/type/index.ts
@@ -119,6 +119,7 @@ export type {
   GraphQLScalarSerializer,
   GraphQLScalarValueParser,
   GraphQLScalarLiteralParser,
+  GraphQLDefaultValueUsage,
 } from './definition.js';
 
 export {
diff --git a/src/type/introspection.ts b/src/type/introspection.ts
index e6e121e1..b72b162c 100644
--- a/src/type/introspection.ts
+++ b/src/type/introspection.ts
@@ -407,8 +407,13 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({
           'A GraphQL-formatted string representing the default value for this input value.',
         resolve(inputValue) {
           const { type, defaultValue } = inputValue;
-          const valueAST = astFromValue(defaultValue, type);
-          return valueAST ? print(valueAST) : null;
+          if (!defaultValue) {
+            return null;
+          }
+          const literal =
+            defaultValue.literal ?? astFromValue(defaultValue.value, type);
+          invariant(literal != null, 'Invalid default value');
+          return print(literal);
         },
       },
       isDeprecated: {
diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts
index 75a4eab9..e49733f0 100644
--- a/src/utilities/TypeInfo.ts
+++ b/src/utilities/TypeInfo.ts
@@ -14,6 +14,7 @@ import { getEnterLeaveForKind } from '../language/visitor.js';
 import type {
   GraphQLArgument,
   GraphQLCompositeType,
+  GraphQLDefaultValueUsage,
   GraphQLEnumValue,
   GraphQLField,
   GraphQLInputField,
@@ -53,7 +54,7 @@ export class TypeInfo {
   private _parentTypeStack: Array<Maybe<GraphQLCompositeType>>;
   private _inputTypeStack: Array<Maybe<GraphQLInputType>>;
   private _fieldDefStack: Array<Maybe<GraphQLField<unknown, unknown>>>;
-  private _defaultValueStack: Array<Maybe<unknown>>;
+  private _defaultValueStack: Array<GraphQLDefaultValueUsage | undefined>;
   private _directive: Maybe<GraphQLDirective>;
   private _argument: Maybe<GraphQLArgument>;
   private _enumValue: Maybe<GraphQLEnumValue>;
@@ -124,7 +125,7 @@ export class TypeInfo {
     return this._fieldDefStack.at(-1);
   }
 
-  getDefaultValue(): Maybe<unknown> {
+  getDefaultValue(): GraphQLDefaultValueUsage | undefined {
     return this._defaultValueStack.at(-1);
   }
 
diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts
index c109b18f..f1f52e8a 100644
--- a/src/utilities/buildClientSchema.ts
+++ b/src/utilities/buildClientSchema.ts
@@ -32,7 +32,6 @@ import { specifiedScalarTypes } from '../type/scalars.js';
 import type { GraphQLSchemaValidationOptions } from '../type/schema.js';
 import { GraphQLSchema } from '../type/schema.js';
 
-import { coerceInputLiteral } from './coerceInputValue.js';
 import type {
   IntrospectionDirective,
   IntrospectionEnumType,
@@ -374,17 +373,13 @@ export function buildClientSchema(
       );
     }
 
-    const defaultValue =
-      inputValueIntrospection.defaultValue != null
-        ? coerceInputLiteral(
-            parseConstValue(inputValueIntrospection.defaultValue),
-            type,
-          )
-        : undefined;
     return {
       description: inputValueIntrospection.description,
       type,
-      defaultValue,
+      defaultValueLiteral:
+        inputValueIntrospection.defaultValue != null
+          ? parseConstValue(inputValueIntrospection.defaultValue)
+          : undefined,
       deprecationReason: inputValueIntrospection.deprecationReason,
     };
   }
diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts
index 88c97c84..f912fea4 100644
--- a/src/utilities/coerceInputValue.ts
+++ b/src/utilities/coerceInputValue.ts
@@ -15,7 +15,10 @@ import { GraphQLError } from '../error/GraphQLError.js';
 import type { ValueNode, VariableNode } from '../language/ast.js';
 import { Kind } from '../language/kinds.js';
 
-import type { GraphQLInputType } from '../type/definition.js';
+import type {
+  GraphQLDefaultValueUsage,
+  GraphQLInputType,
+} from '../type/definition.js';
 import {
   assertLeafType,
   isInputObjectType,
@@ -111,8 +114,11 @@ function coerceInputValueImpl(
       const fieldValue = inputValue[field.name];
 
       if (fieldValue === undefined) {
-        if (field.defaultValue !== undefined) {
-          coercedValue[field.name] = field.defaultValue;
+        if (field.defaultValue) {
+          coercedValue[field.name] = coerceDefaultValue(
+            field.defaultValue,
+            field.type,
+          );
         } else if (isNonNullType(field.type)) {
           const typeStr = inspect(field.type);
           onError(
@@ -326,8 +332,11 @@ export function coerceInputLiteral(
         if (isRequiredInputField(field)) {
           return; // Invalid: intentionally return no value.
         }
-        if (field.defaultValue !== undefined) {
-          coercedValue[field.name] = field.defaultValue;
+        if (field.defaultValue) {
+          coercedValue[field.name] = coerceDefaultValue(
+            field.defaultValue,
+            field.type,
+          );
         }
       } else {
         const fieldValue = coerceInputLiteral(
@@ -379,3 +388,21 @@ function getVariableValue(
 
   return variableValues?.[varName];
 }
+
+/**
+ * @internal
+ */
+export function coerceDefaultValue(
+  defaultValue: GraphQLDefaultValueUsage,
+  type: GraphQLInputType,
+): unknown {
+  // Memoize the result of coercing the default value in a hidden field.
+  let coercedValue = (defaultValue as any)._memoizedCoercedValue;
+  if (coercedValue === undefined) {
+    coercedValue = defaultValue.literal
+      ? coerceInputLiteral(defaultValue.literal, type)
+      : defaultValue.value;
+    (defaultValue as any)._memoizedCoercedValue = coercedValue;
+  }
+  return coercedValue;
+}
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index d18b53d0..f43c1533 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -83,8 +83,6 @@ import { assertValidSDLExtension } from '../validation/validate.js';
 
 import { getDirectiveValues } from '../execution/values.js';
 
-import { coerceInputLiteral } from './coerceInputValue.js';
-
 interface Options extends GraphQLSchemaValidationOptions {
   /**
    * Set to true to assume the SDL is valid.
@@ -535,9 +533,7 @@ export function extendSchemaImpl(
       argConfigMap[arg.name.value] = {
         type,
         description: arg.description?.value,
-        defaultValue: arg.defaultValue
-          ? coerceInputLiteral(arg.defaultValue, type)
-          : undefined,
+        defaultValueLiteral: arg.defaultValue,
         deprecationReason: getDeprecationReason(arg),
         astNode: arg,
       };
@@ -564,9 +560,7 @@ export function extendSchemaImpl(
         inputFieldMap[field.name.value] = {
           type,
           description: field.description?.value,
-          defaultValue: field.defaultValue
-            ? coerceInputLiteral(field.defaultValue, type)
-            : undefined,
+          defaultValueLiteral: field.defaultValue,
           deprecationReason: getDeprecationReason(field),
           astNode: field,
         };
diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts
index 29210746..e6f4f790 100644
--- a/src/utilities/findBreakingChanges.ts
+++ b/src/utilities/findBreakingChanges.ts
@@ -5,6 +5,7 @@ import { keyMap } from '../jsutils/keyMap.js';
 import { print } from '../language/printer.js';
 
 import type {
+  GraphQLDefaultValueUsage,
   GraphQLEnumType,
   GraphQLField,
   GraphQLInputObjectType,
@@ -534,8 +535,11 @@ function typeKindName(type: GraphQLNamedType): string {
   invariant(false, 'Unexpected type: ' + inspect(type));
 }
 
-function stringifyValue(value: unknown, type: GraphQLInputType): string {
-  const ast = astFromValue(value, type);
+function stringifyValue(
+  defaultValue: GraphQLDefaultValueUsage,
+  type: GraphQLInputType,
+): string {
+  const ast = defaultValue.literal ?? astFromValue(defaultValue.value, type);
   invariant(ast != null);
   return print(sortValueNode(ast));
 }
diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts
index 7d4a5e4f..b02580c6 100644
--- a/src/utilities/printSchema.ts
+++ b/src/utilities/printSchema.ts
@@ -259,10 +259,13 @@ function printArgs(
 }
 
 function printInputValue(arg: GraphQLInputField): string {
-  const defaultAST = astFromValue(arg.defaultValue, arg.type);
   let argDecl = arg.name + ': ' + String(arg.type);
-  if (defaultAST) {
-    argDecl += ` = ${print(defaultAST)}`;
+  if (arg.defaultValue) {
+    const literal =
+      arg.defaultValue.literal ??
+      astFromValue(arg.defaultValue.value, arg.type);
+    invariant(literal != null, 'Invalid default value');
+    argDecl += ` = ${print(literal)}`;
   }
   return argDecl + printDeprecated(arg.deprecationReason);
 }
diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts
index add91536..e0ff5d8b 100644
--- a/src/utilities/valueFromAST.ts
+++ b/src/utilities/valueFromAST.ts
@@ -115,7 +115,7 @@ export function valueFromAST(
       const fieldNode = fieldNodes.get(field.name);
       if (fieldNode == null || isMissingVariable(fieldNode.value, variables)) {
         if (field.defaultValue !== undefined) {
-          coercedObj[field.name] = field.defaultValue;
+          coercedObj[field.name] = field.defaultValue.value;
         } else if (isNonNullType(field.type)) {
           return; // Invalid: intentionally return no value.
         }
diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts
index 7f7114c4..d45e7a46 100644
--- a/src/validation/ValidationContext.ts
+++ b/src/validation/ValidationContext.ts
@@ -19,6 +19,7 @@ import { visit } from '../language/visitor.js';
 import type {
   GraphQLArgument,
   GraphQLCompositeType,
+  GraphQLDefaultValueUsage,
   GraphQLEnumValue,
   GraphQLField,
   GraphQLInputType,
@@ -34,7 +35,7 @@ type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode;
 interface VariableUsage {
   readonly node: VariableNode;
   readonly type: Maybe<GraphQLInputType>;
-  readonly defaultValue: Maybe<unknown>;
+  readonly defaultValue: GraphQLDefaultValueUsage | undefined;
   readonly fragmentVariableDefinition: Maybe<VariableDefinitionNode>;
 }
 
diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
index 0d9870c7..dae909c6 100644
--- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
+++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
@@ -65,10 +65,14 @@ function reasonMessage(reason: ConflictReasonMessage): string {
 export function OverlappingFieldsCanBeMergedRule(
   context: ValidationContext,
 ): ASTVisitor {
-  // A memoization for when two fragments are compared "between" each other for
-  // conflicts. Two fragments may be compared many times, so memoizing this can
-  // dramatically improve the performance of this validator.
-  const comparedFragmentPairs = new PairSet();
+  // A memoization for when fields and a fragment or two fragments are compared
+  // "between" each other for conflicts. Comparisons made be made many times,
+  // so memoizing this can dramatically improve the performance of this validator.
+  const comparedFieldsAndFragmentPairs = new OrderedPairSet<
+    NodeAndDefCollection,
+    string
+  >();
+  const comparedFragmentPairs = new PairSet<string>();
 
   // A cache for the "field map" and list of fragment spreads found in any given
   // selection set. Selection sets may be asked for this information multiple
@@ -80,6 +84,7 @@ export function OverlappingFieldsCanBeMergedRule(
       const conflicts = findConflictsWithinSelectionSet(
         context,
         cachedFieldsAndFragmentSpreads,
+        comparedFieldsAndFragmentPairs,
         comparedFragmentPairs,
         context.getParentType(),
         selectionSet,
@@ -185,7 +190,8 @@ function findConflictsWithinSelectionSet(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   parentType: Maybe<GraphQLNamedType>,
   selectionSet: SelectionSetNode,
 ): Array<Conflict> {
@@ -205,6 +211,7 @@ function findConflictsWithinSelectionSet(
     context,
     conflicts,
     cachedFieldsAndFragmentSpreads,
+    comparedFieldsAndFragmentPairs,
     comparedFragmentPairs,
     fieldMap,
   );
@@ -217,6 +224,7 @@ function findConflictsWithinSelectionSet(
         context,
         conflicts,
         cachedFieldsAndFragmentSpreads,
+        comparedFieldsAndFragmentPairs,
         comparedFragmentPairs,
         false,
         fieldMap,
@@ -231,6 +239,7 @@ function findConflictsWithinSelectionSet(
           context,
           conflicts,
           cachedFieldsAndFragmentSpreads,
+          comparedFieldsAndFragmentPairs,
           comparedFragmentPairs,
           false,
           fragmentSpreads[i],
@@ -251,11 +260,29 @@ function collectConflictsBetweenFieldsAndFragment(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   areMutuallyExclusive: boolean,
   fieldMap: NodeAndDefCollection,
   fragmentSpread: FragmentSpread,
 ): void {
+  // Memoize so the fields and fragments are not compared for conflicts more
+  // than once.
+  if (
+    comparedFieldsAndFragmentPairs.has(
+      fieldMap,
+      fragmentSpread.key,
+      areMutuallyExclusive,
+    )
+  ) {
+    return;
+  }
+  comparedFieldsAndFragmentPairs.add(
+    fieldMap,
+    fragmentSpread.key,
+    areMutuallyExclusive,
+  );
+
   const fragment = context.getFragment(fragmentSpread.node.name.value);
   if (!fragment) {
     return;
@@ -280,6 +307,7 @@ function collectConflictsBetweenFieldsAndFragment(
     context,
     conflicts,
     cachedFieldsAndFragmentSpreads,
+    comparedFieldsAndFragmentPairs,
     comparedFragmentPairs,
     areMutuallyExclusive,
     fieldMap,
@@ -289,28 +317,13 @@ function collectConflictsBetweenFieldsAndFragment(
   );
 
   // (E) Then collect any conflicts between the provided collection of fields
-  // and any fragment spreads found in the given fragment.
+  // and any fragment names found in the given fragment.
   for (const referencedFragmentSpread of referencedFragmentSpreads) {
-    // Memoize so two fragments are not compared for conflicts more than once.
-    if (
-      comparedFragmentPairs.has(
-        referencedFragmentSpread.key,
-        fragmentSpread.key,
-        areMutuallyExclusive,
-      )
-    ) {
-      continue;
-    }
-    comparedFragmentPairs.add(
-      referencedFragmentSpread.key,
-      fragmentSpread.key,
-      areMutuallyExclusive,
-    );
-
     collectConflictsBetweenFieldsAndFragment(
       context,
       conflicts,
       cachedFieldsAndFragmentSpreads,
+      comparedFieldsAndFragmentPairs,
       comparedFragmentPairs,
       areMutuallyExclusive,
       fieldMap,
@@ -328,7 +341,8 @@ function collectConflictsBetweenFragments(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   areMutuallyExclusive: boolean,
   fragmentSpread1: FragmentSpread,
   fragmentSpread2: FragmentSpread,
@@ -400,6 +414,7 @@ function collectConflictsBetweenFragments(
     context,
     conflicts,
     cachedFieldsAndFragmentSpreads,
+    comparedFieldsAndFragmentPairs,
     comparedFragmentPairs,
     areMutuallyExclusive,
     fieldMap1,
@@ -415,6 +430,7 @@ function collectConflictsBetweenFragments(
       context,
       conflicts,
       cachedFieldsAndFragmentSpreads,
+      comparedFieldsAndFragmentPairs,
       comparedFragmentPairs,
       areMutuallyExclusive,
       fragmentSpread1,
@@ -429,6 +445,7 @@ function collectConflictsBetweenFragments(
       context,
       conflicts,
       cachedFieldsAndFragmentSpreads,
+      comparedFieldsAndFragmentPairs,
       comparedFragmentPairs,
       areMutuallyExclusive,
       referencedFragmentSpread1,
@@ -446,7 +463,8 @@ function findConflictsBetweenSubSelectionSets(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   areMutuallyExclusive: boolean,
   parentType1: Maybe<GraphQLNamedType>,
   selectionSet1: SelectionSetNode,
@@ -477,6 +495,7 @@ function findConflictsBetweenSubSelectionSets(
     context,
     conflicts,
     cachedFieldsAndFragmentSpreads,
+    comparedFieldsAndFragmentPairs,
     comparedFragmentPairs,
     areMutuallyExclusive,
     fieldMap1,
@@ -492,6 +511,7 @@ function findConflictsBetweenSubSelectionSets(
       context,
       conflicts,
       cachedFieldsAndFragmentSpreads,
+      comparedFieldsAndFragmentPairs,
       comparedFragmentPairs,
       areMutuallyExclusive,
       fieldMap1,
@@ -506,6 +526,7 @@ function findConflictsBetweenSubSelectionSets(
       context,
       conflicts,
       cachedFieldsAndFragmentSpreads,
+      comparedFieldsAndFragmentPairs,
       comparedFragmentPairs,
       areMutuallyExclusive,
       fieldMap2,
@@ -522,6 +543,7 @@ function findConflictsBetweenSubSelectionSets(
         context,
         conflicts,
         cachedFieldsAndFragmentSpreads,
+        comparedFieldsAndFragmentPairs,
         comparedFragmentPairs,
         areMutuallyExclusive,
         fragmentSpread1,
@@ -540,7 +562,8 @@ function collectConflictsWithin(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   fieldMap: NodeAndDefCollection,
 ): void {
   // A field map is a keyed collection, where each key represents a response
@@ -557,6 +580,7 @@ function collectConflictsWithin(
           const conflict = findConflict(
             context,
             cachedFieldsAndFragmentSpreads,
+            comparedFieldsAndFragmentPairs,
             comparedFragmentPairs,
             false, // within one collection is never mutually exclusive
             responseName,
@@ -586,7 +610,8 @@ function collectConflictsBetween(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   parentFieldsAreMutuallyExclusive: boolean,
   fieldMap1: NodeAndDefCollection,
   varMap1: Map<string, ValueNode> | undefined,
@@ -606,6 +631,7 @@ function collectConflictsBetween(
           const conflict = findConflict(
             context,
             cachedFieldsAndFragmentSpreads,
+            comparedFieldsAndFragmentPairs,
             comparedFragmentPairs,
             parentFieldsAreMutuallyExclusive,
             responseName,
@@ -631,7 +657,8 @@ function findConflict(
     SelectionSetNode,
     FieldsAndFragmentSpreads
   >,
-  comparedFragmentPairs: PairSet,
+  comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
+  comparedFragmentPairs: PairSet<string>,
   parentFieldsAreMutuallyExclusive: boolean,
   responseName: string,
   field1: NodeAndDef,
@@ -715,6 +742,7 @@ function findConflict(
     const conflicts = findConflictsBetweenSubSelectionSets(
       context,
       cachedFieldsAndFragmentSpreads,
+      comparedFieldsAndFragmentPairs,
       comparedFragmentPairs,
       areMutuallyExclusive,
       getNamedType(type1),
@@ -1030,37 +1058,60 @@ function subfieldConflicts(
 }
 
 /**
- * A way to keep track of pairs of things when the ordering of the pair does not matter.
+ * A way to keep track of pairs of things where the ordering of the pair
+ * matters.
+ *
+ * Provides a third argument for has/set to allow flagging the pair as
+ * weakly or strongly present within the collection.
  */
-class PairSet {
-  _data: Map<string, Map<string, boolean>>;
+class OrderedPairSet<T, U> {
+  _data: Map<T, Map<U, boolean>>;
 
   constructor() {
     this._data = new Map();
   }
 
-  has(a: string, b: string, areMutuallyExclusive: boolean): boolean {
-    const [key1, key2] = a < b ? [a, b] : [b, a];
-
-    const result = this._data.get(key1)?.get(key2);
+  has(a: T, b: U, weaklyPresent: boolean): boolean {
+    const result = this._data.get(a)?.get(b);
     if (result === undefined) {
       return false;
     }
 
-    // areMutuallyExclusive being false is a superset of being true, hence if
-    // we want to know if this PairSet "has" these two with no exclusivity,
-    // we have to ensure it was added as such.
-    return areMutuallyExclusive ? true : areMutuallyExclusive === result;
+    return weaklyPresent ? true : weaklyPresent === result;
   }
 
-  add(a: string, b: string, areMutuallyExclusive: boolean): void {
-    const [key1, key2] = a < b ? [a, b] : [b, a];
-
-    const map = this._data.get(key1);
+  add(a: T, b: U, weaklyPresent: boolean): void {
+    const map = this._data.get(a);
     if (map === undefined) {
-      this._data.set(key1, new Map([[key2, areMutuallyExclusive]]));
+      this._data.set(a, new Map([[b, weaklyPresent]]));
+    } else {
+      map.set(b, weaklyPresent);
+    }
+  }
+}
+
+/**
+ * A way to keep track of pairs of similar things when the ordering of the pair
+ * does not matter.
+ */
+class PairSet<T> {
+  _orderedPairSet: OrderedPairSet<T, T>;
+
+  constructor() {
+    this._orderedPairSet = new OrderedPairSet();
+  }
+
+  has(a: T, b: T, weaklyPresent: boolean): boolean {
+    return a < b
+      ? this._orderedPairSet.has(a, b, weaklyPresent)
+      : this._orderedPairSet.has(b, a, weaklyPresent);
+  }
+
+  add(a: T, b: T, weaklyPresent: boolean): void {
+    if (a < b) {
+      this._orderedPairSet.add(a, b, weaklyPresent);
     } else {
-      map.set(key2, areMutuallyExclusive);
+      this._orderedPairSet.add(b, a, weaklyPresent);
     }
   }
 }
diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts
index e68a595c..888e49d3 100644
--- a/src/validation/rules/VariablesInAllowedPositionRule.ts
+++ b/src/validation/rules/VariablesInAllowedPositionRule.ts
@@ -7,7 +7,10 @@ import type { ValueNode, VariableDefinitionNode } from '../../language/ast.js';
 import { Kind } from '../../language/kinds.js';
 import type { ASTVisitor } from '../../language/visitor.js';
 
-import type { GraphQLType } from '../../type/definition.js';
+import type {
+  GraphQLDefaultValueUsage,
+  GraphQLType,
+} from '../../type/definition.js';
 import { isNonNullType } from '../../type/definition.js';
 import type { GraphQLSchema } from '../../type/schema.js';
 
@@ -95,7 +98,7 @@ function allowedVariableUsage(
   varType: GraphQLType,
   varDefaultValue: Maybe<ValueNode>,
   locationType: GraphQLType,
-  locationDefaultValue: Maybe<unknown>,
+  locationDefaultValue: GraphQLDefaultValueUsage | undefined,
 ): boolean {
   if (isNonNullType(locationType) && !isNonNullType(varType)) {
     const hasNonNullVariableDefaultValue =


Test patch:
diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts
index 9a491fc3..c5e6ebf3 100644
--- a/src/type/__tests__/definition-test.ts
+++ b/src/type/__tests__/definition-test.ts
@@ -4,6 +4,7 @@ import { describe, it } from 'mocha';
 import { identityFunc } from '../../jsutils/identityFunc.js';
 import { inspect } from '../../jsutils/inspect.js';
 
+import { Kind } from '../../language/kinds.js';
 import { parseValue } from '../../language/parser.js';
 
 import type { GraphQLNullableType, GraphQLType } from '../definition.js';
@@ -581,6 +582,63 @@ describe('Type System: Input Objects', () => {
       'not used anymore',
     );
   });
+
+  describe('Input Object fields may have default values', () => {
+    it('accepts an Input Object type with a default value', () => {
+      const inputObjType = new GraphQLInputObjectType({
+        name: 'SomeInputObject',
+        fields: {
+          f: { type: ScalarType, defaultValue: 3 },
+        },
+      });
+      expect(inputObjType.getFields().f).to.deep.include({
+        name: 'f',
+        description: undefined,
+        type: ScalarType,
+        defaultValue: { value: 3 },
+        deprecationReason: undefined,
+        extensions: {},
+        astNode: undefined,
+      });
+    });
+
+    it('accepts an Input Object type with a default value literal', () => {
+      const inputObjType = new GraphQLInputObjectType({
+        name: 'SomeInputObject',
+        fields: {
+          f: {
+            type: ScalarType,
+            defaultValueLiteral: { kind: Kind.INT, value: '3' },
+          },
+        },
+      });
+      expect(inputObjType.getFields().f).to.deep.include({
+        name: 'f',
+        description: undefined,
+        type: ScalarType,
+        defaultValue: { literal: { kind: 'IntValue', value: '3' } },
+        deprecationReason: undefined,
+        extensions: {},
+        astNode: undefined,
+      });
+    });
+
+    it('rejects an Input Object type with potentially conflicting default values', () => {
+      const inputObjType = new GraphQLInputObjectType({
+        name: 'SomeInputObject',
+        fields: {
+          f: {
+            type: ScalarType,
+            defaultValue: 3,
+            defaultValueLiteral: { kind: Kind.INT, value: '3' },
+          },
+        },
+      });
+      expect(() => inputObjType.getFields()).to.throw(
+        'Argument "f" has both a defaultValue and a defaultValueLiteral property, but only one must be provided.',
+      );
+    });
+  });
 });
 
 describe('Type System: List', () => {
diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts
index c1e1fb30..a71ce8c0 100644
--- a/src/type/__tests__/predicate-test.ts
+++ b/src/type/__tests__/predicate-test.ts
@@ -574,7 +574,10 @@ describe('Type predicates', () => {
         name: 'someArg',
         type: config.type,
         description: undefined,
-        defaultValue: config.defaultValue,
+        defaultValue:
+          config.defaultValue !== undefined
+            ? { value: config.defaultValue }
+            : undefined,
         deprecationReason: null,
         extensions: Object.create(null),
         astNode: undefined,
@@ -622,7 +625,10 @@ describe('Type predicates', () => {
         name: 'someInputField',
         type: config.type,
         description: undefined,
-        defaultValue: config.defaultValue,
+        defaultValue:
+          config.defaultValue !== undefined
+            ? { value: config.defaultValue }
+            : undefined,
         deprecationReason: null,
         extensions: Object.create(null),
         astNode: undefined,
diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts
index b6b43362..0ad292d0 100644
--- a/src/utilities/__tests__/buildClientSchema-test.ts
+++ b/src/utilities/__tests__/buildClientSchema-test.ts
@@ -439,6 +439,7 @@ describe('Type System: build schema from introspection', () => {
       }
 
       type Query {
+        defaultID(intArg: ID = "123"): String
         defaultInt(intArg: Int = 30): String
         defaultList(listArg: [Int] = [1, 2, 3]): String
         defaultObject(objArg: Geo = { lat: 37.485, lon: -122.148 }): String
@@ -609,6 +610,28 @@ describe('Type System: build schema from introspection', () => {
     expect(result.data).to.deep.equal({ foo: 'bar' });
   });
 
+  it('can use client schema for execution if resolvers are added', () => {
+    const schema = buildSchema(`
+      type Query {
+        foo(bar: String = "abc"): String
+      }
+    `);
+
+    const introspection = introspectionFromSchema(schema);
+    const clientSchema = buildClientSchema(introspection);
+
+    const QueryType: GraphQLObjectType = clientSchema.getType('Query') as any;
+    QueryType.getFields().foo.resolve = (_value, args) => args.bar;
+
+    const result = graphqlSync({
+      schema: clientSchema,
+      source: '{ foo }',
+    });
+
+    expect(result.data).to.deep.equal({ foo: 'abc' });
+    expect(result.data).to.deep.equal({ foo: 'abc' });
+  });
+
   it('can build invalid schema', () => {
     const schema = buildSchema('type Query', { assumeValid: true });
 
diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts
index 15467e84..78dede7b 100644
--- a/src/utilities/__tests__/coerceInputValue-test.ts
+++ b/src/utilities/__tests__/coerceInputValue-test.ts
@@ -5,6 +5,7 @@ import { identityFunc } from '../../jsutils/identityFunc.js';
 import { invariant } from '../../jsutils/invariant.js';
 import type { ObjMap } from '../../jsutils/ObjMap.js';
 
+import { Kind } from '../../language/kinds.js';
 import { parseValue } from '../../language/parser.js';
 import { print } from '../../language/printer.js';
 
@@ -24,7 +25,11 @@ import {
   GraphQLString,
 } from '../../type/scalars.js';
 
-import { coerceInputLiteral, coerceInputValue } from '../coerceInputValue.js';
+import {
+  coerceDefaultValue,
+  coerceInputLiteral,
+  coerceInputValue,
+} from '../coerceInputValue.js';
 
 interface CoerceResult {
   value: unknown;
@@ -716,10 +721,14 @@ describe('coerceInputLiteral', () => {
       name: 'TestInput',
       fields: {
         int: { type: GraphQLInt, defaultValue: 42 },
+        float: {
+          type: GraphQLFloat,
+          defaultValueLiteral: { kind: Kind.FLOAT, value: '3.14' },
+        },
       },
     });
 
-    test('{}', type, { int: 42 });
+    test('{}', type, { int: 42, float: 3.14 });
   });
 
   const testInputObj = new GraphQLInputObjectType({
@@ -807,3 +816,26 @@ describe('coerceInputLiteral', () => {
     });
   });
 });
+
+describe('coerceDefaultValue', () => {
+  it('memoizes coercion', () => {
+    const parseValueCalls: any = [];
+
+    const spyScalar = new GraphQLScalarType({
+      name: 'SpyScalar',
+      parseValue(value) {
+        parseValueCalls.push(value);
+        return value;
+      },
+    });
+
+    const defaultValueUsage = {
+      literal: { kind: Kind.STRING, value: 'hello' },
+    } as const;
+    expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello');
+
+    // Call a second time
+    expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello');
+    expect(parseValueCalls).to.deep.equal(['hello']);
+  });
+});
diff --git a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
index 7d7c54f3..582e4602 100644
--- a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
+++ b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
@@ -638,10 +638,10 @@ describe('Validate: Overlapping fields can be merged', () => {
     expectErrors(`
       {
         field {
-          ...F
+          ...I
         }
         field {
-          ...I
+          ...F
         }
       }
       fragment F on T {
@@ -661,14 +661,41 @@ describe('Validate: Overlapping fields can be merged', () => {
     `).toDeepEqual([
       {
         message:
-          'Fields "field" conflict because subfields "x" conflict because "a" and "b" are different fields and subfields "y" conflict because "c" and "d" are different fields. Use different aliases on the fields to fetch both if this was intentional.',
+          'Fields "field" conflict because subfields "y" conflict because "d" and "c" are different fields and subfields "x" conflict because "b" and "a" are different fields. Use different aliases on the fields to fetch both if this was intentional.',
         locations: [
           { line: 3, column: 9 },
-          { line: 11, column: 9 },
-          { line: 15, column: 9 },
-          { line: 6, column: 9 },
-          { line: 22, column: 9 },
           { line: 18, column: 9 },
+          { line: 22, column: 9 },
+          { line: 6, column: 9 },
+          { line: 15, column: 9 },
+          { line: 11, column: 9 },
+        ],
+      },
+    ]);
+  });
+
+  it('reports deep conflict after nested fragments', () => {
+    expectErrors(`
+      fragment F on T {
+        ...G
+      }
+      fragment G on T {
+        ...H
+      }
+      fragment H on T {
+        x: a
+      }
+      {
+        x: b
+        ...F
+      }
+    `).toDeepEqual([
+      {
+        message:
+          'Fields "x" conflict because "b" and "a" are different fields. Use different aliases on the fields to fetch both if this was intentional.',
+        locations: [
+          { line: 12, column: 9 },
+          { line: 9, column: 9 },
         ],
       },
     ]);
@@ -1265,6 +1292,33 @@ describe('Validate: Overlapping fields can be merged', () => {
     ]);
   });
 
+  it('does not infinite loop on recursive fragments separated by fields', () => {
+    expectValid(`
+      {
+        ...fragA
+        ...fragB
+      }
+
+      fragment fragA on T {
+        x {
+          ...fragA
+          x {
+            ...fragA
+          }
+        }
+      }
+
+      fragment fragB on T {
+        x {
+          ...fragB
+          x {
+            ...fragB
+          }
+        }
+      }
+    `);
+  });
+
   describe('fragment arguments must produce fields that can be merged', () => {
     it('allows conflicting spreads at different depths', () => {
       expectValid(`