From 7f426f0488223db769f367642962ebfa57c8d904 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Mon, 28 Sep 2026 16:18:20 +0200 Subject: [PATCH 1/5] Add support for empty object types (cherry picked from commit 238dbc975373d464687541aa79b2f5eee3bb992f) --- src/language/__tests__/schema-parser-test.ts | 38 ++++++++++ src/language/parser.ts | 30 ++++++++ src/type/__tests__/validation-test.ts | 75 +++++++++++++++++++ src/type/schema.ts | 18 +++++ src/type/validate.ts | 8 +- .../__tests__/buildClientSchema-test.ts | 27 +++++++ src/utilities/buildASTSchema.ts | 3 + src/utilities/buildClientSchema.ts | 2 + src/utilities/extendSchema.ts | 3 + 9 files changed, 202 insertions(+), 2 deletions(-) diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts index cbedcb3030..618aba36af 100644 --- a/src/language/__tests__/schema-parser-test.ts +++ b/src/language/__tests__/schema-parser-test.ts @@ -110,6 +110,44 @@ describe('Schema Parser', () => { }); }); + it('rejects empty fields definition by default', () => { + expectSyntaxError('type Hello {}').to.deep.equal({ + message: 'Syntax Error: Expected Name, found "}".', + locations: [{ line: 1, column: 13 }], + }); + }); + + it('parses empty fields definition with experimentalAllowEmptyObjectTypes', () => { + const doc = parse('type Hello {}', { + experimentalAllowEmptyObjectTypes: true, + }); + + expectJSON(doc).toDeepEqual({ + kind: 'Document', + definitions: [ + { + kind: 'ObjectTypeDefinition', + name: nameNode('Hello', { start: 5, end: 10 }), + description: undefined, + interfaces: undefined, + directives: undefined, + fields: [], + loc: { start: 0, end: 13 }, + }, + ], + loc: { start: 0, end: 13 }, + }); + }); + + it('parses empty fields definition on interfaces and extensions with experimentalAllowEmptyObjectTypes', () => { + const doc = parse('interface Hello {} extend type World {}', { + experimentalAllowEmptyObjectTypes: true, + }); + + expectJSON(doc).toDeepNestedProperty('definitions[0].fields', []); + expectJSON(doc).toDeepNestedProperty('definitions[1].fields', []); + }); + it('parses type with description string', () => { const doc = parse(dedent` "Description" diff --git a/src/language/parser.ts b/src/language/parser.ts index a5cba948cc..d5e398f452 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -161,6 +161,20 @@ export interface ParseOptions { * @internal */ lexer?: LexerInterface | undefined; + + /** + * EXPERIMENTAL: + * + * If enabled, the parser accepts an empty set of braces as a fields + * definition, as proposed in https://github.com/graphql/graphql-spec/pull/1245 + * + * ```graphql + * type Empty {} + * ``` + * + * This is equivalent to omitting the braces entirely (`type Empty`). + */ + experimentalAllowEmptyObjectTypes?: boolean | undefined; } /** @@ -1204,9 +1218,25 @@ export class Parser { * FieldsDefinition : { FieldDefinition+ } * ``` * + * With `experimentalAllowEmptyObjectTypes`: + * + * ``` + * FieldsDefinition : { FieldDefinition* } + * ``` + * * @internal */ parseFieldsDefinition(): Array | undefined { + if ( + this._options.experimentalAllowEmptyObjectTypes === true && + this.peek(TokenKind.BRACE_L) + ) { + return this.any( + TokenKind.BRACE_L, + this.parseFieldDefinition, + TokenKind.BRACE_R, + ); + } return this.optionalMany( TokenKind.BRACE_L, this.parseFieldDefinition, diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index a5bd4012fe..8a6256b8c7 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -589,6 +589,81 @@ describe('Type System: Objects must have fields', () => { ]); }); + it('accepts an Object type with no fields when experimentalAllowEmptyObjectTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyObject + } + + type EmptyObject + `, + { experimentalAllowEmptyObjectTypes: true }, + ); + expect(schema.experimentalAllowEmptyObjectTypes).to.equal(true); + expectJSON(validateSchema(schema)).toDeepEqual([]); + + const manualSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + test: { + type: new GraphQLObjectType({ name: 'EmptyObject', fields: {} }), + }, + }, + }), + experimentalAllowEmptyObjectTypes: true, + }); + expectJSON(validateSchema(manualSchema)).toDeepEqual([]); + + const extendedSchema = extendSchema( + schema, + parse('type AnotherEmptyObject'), + ); + expect(extendedSchema.experimentalAllowEmptyObjectTypes).to.equal(true); + expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); + }); + + it('accepts an Object type with empty braces when experimentalAllowEmptyObjectTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyObject + } + + type EmptyObject {} + `, + { experimentalAllowEmptyObjectTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('accepts an empty Query root type when experimentalAllowEmptyObjectTypes is enabled', () => { + const schema = buildSchema('type Query', { + experimentalAllowEmptyObjectTypes: true, + }); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('rejects an Interface type with no fields even when experimentalAllowEmptyObjectTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyInterface + } + + interface EmptyInterface + `, + { experimentalAllowEmptyObjectTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: 'Type EmptyInterface must define one or more fields.', + locations: [{ line: 6, column: 7 }], + }, + ]); + }); + it('rejects an Object type with incorrectly named fields', () => { const schema = schemaWithFieldType( new GraphQLObjectType({ diff --git a/src/type/schema.ts b/src/type/schema.ts index 59cf87011e..34249ba7a8 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -229,6 +229,8 @@ export class GraphQLSchema { /** Whether this schema instance skips validation checks. */ assumeValid: boolean; + /** Whether object types are allowed to define no fields. */ + experimentalAllowEmptyObjectTypes: boolean; /** * Cached schema validation errors, if validation has already run. * @private @@ -345,6 +347,8 @@ export class GraphQLSchema { this.assumeValid = config.assumeValid ?? false; // Used as a cache for validateSchema(). this.__validationErrors = config.assumeValid === true ? [] : undefined; + this.experimentalAllowEmptyObjectTypes = + config.experimentalAllowEmptyObjectTypes ?? false; this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -886,6 +890,7 @@ export class GraphQLSchema { astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, assumeValid: this.assumeValid, + experimentalAllowEmptyObjectTypes: this.experimentalAllowEmptyObjectTypes, }; } } @@ -904,6 +909,18 @@ export interface GraphQLSchemaValidationOptions { * @internal */ assumeValid?: boolean | undefined; + + /** + * EXPERIMENTAL: + * + * If enabled, object types are allowed to define no fields, as proposed in + * https://github.com/graphql/graphql-spec/pull/1228 + * + * Interface types and input object types must still define one or more fields. + * + * Default: false + */ + experimentalAllowEmptyObjectTypes?: boolean | undefined; } /** Configuration used to construct a GraphQLSchema. */ @@ -936,6 +953,7 @@ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { extensions: Readonly; extensionASTNodes: ReadonlyArray; assumeValid: boolean; + experimentalAllowEmptyObjectTypes: boolean; } function collectReferencedTypes( diff --git a/src/type/validate.ts b/src/type/validate.ts index 8289b9df31..6db90f7b94 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -476,8 +476,12 @@ function validateFields( ): void { const fields = Object.values(type.getFields()); - // Objects and Interfaces both must define one or more fields. - if (fields.length === 0) { + // Objects and Interfaces both must define one or more fields, unless empty + // object types are explicitly allowed. + if ( + fields.length === 0 && + !(isObjectType(type) && context.schema.experimentalAllowEmptyObjectTypes) + ) { context.reportError(`Type ${type} must define one or more fields.`, [ type.astNode, ...type.extensionASTNodes, diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 356da136ec..ba75d34b71 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -66,6 +66,33 @@ describe('Type System: build schema from introspection', () => { expect(cycleIntrospection(sdl)).to.equal(sdl); }); + it('builds a schema with an empty object type', () => { + const sdl = dedent` + type Query { + empty: Empty + } + + type Empty + `; + const serverSchema = buildSchema(sdl, { + experimentalAllowEmptyObjectTypes: true, + }); + const introspection = introspectionFromSchema(serverSchema); + const clientSchema = buildClientSchema(introspection, { + experimentalAllowEmptyObjectTypes: true, + }); + + expect(clientSchema.experimentalAllowEmptyObjectTypes).to.equal(true); + expect(printSchema(clientSchema)).to.equal(sdl); + expect( + graphqlSync({ + schema: clientSchema, + source: '{ empty { __typename } }', + rootValue: { empty: {} }, + }), + ).to.deep.equal({ data: { empty: { __typename: 'Empty' } } }); + }); + it('builds a schema without the query type', () => { const sdl = dedent` type Query { diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index 50a8a5993d..bc91ddefd2 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -75,6 +75,7 @@ export function buildASTSchema( extensions: Object.create(null), extensionASTNodes: [], assumeValid: false, + experimentalAllowEmptyObjectTypes: false, }; const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); @@ -156,5 +157,7 @@ export function buildSchema( return buildASTSchema(document, { assumeValidSDL: options?.assumeValidSDL, assumeValid: options?.assumeValid, + experimentalAllowEmptyObjectTypes: + options?.experimentalAllowEmptyObjectTypes, }); } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 2a1baea37c..c60555d6bf 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -143,6 +143,8 @@ export function buildClientSchema( types: [...typeMap.values()], directives, assumeValid: options?.assumeValid, + experimentalAllowEmptyObjectTypes: + options?.experimentalAllowEmptyObjectTypes, }); // Given a type reference in introspection, return the GraphQLType instance. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 381407e9cf..82e8c11ba7 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -301,6 +301,9 @@ export function extendSchemaImpl( astNode: schemaDef ?? config.astNode, extensionASTNodes: config.extensionASTNodes.concat(schemaExtensions), assumeValid: options?.assumeValid ?? false, + experimentalAllowEmptyObjectTypes: + options?.experimentalAllowEmptyObjectTypes ?? + config.experimentalAllowEmptyObjectTypes, }; }, [SchemaElementKind.INPUT_OBJECT]: (config) => { From 8a23db54e2ed6c109a52aec86aa265a7ca4cdcbc Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Mon, 28 Sep 2026 17:36:52 +0200 Subject: [PATCH 2/5] Allow empty interface types (cherry picked from commit 715c980ed70b9458e67c969ee714491b154b50ed) --- src/language/__tests__/schema-parser-test.ts | 29 +++++++ src/language/parser.ts | 20 ++++- src/type/__tests__/validation-test.ts | 80 +++++++++++++++++++ src/type/schema.ts | 20 ++++- src/type/validate.ts | 10 +-- .../__tests__/buildClientSchema-test.ts | 33 ++++++++ src/utilities/buildASTSchema.ts | 3 + src/utilities/buildClientSchema.ts | 2 + src/utilities/extendSchema.ts | 3 + 9 files changed, 192 insertions(+), 8 deletions(-) diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts index 618aba36af..4255b91837 100644 --- a/src/language/__tests__/schema-parser-test.ts +++ b/src/language/__tests__/schema-parser-test.ts @@ -148,6 +148,35 @@ describe('Schema Parser', () => { expectJSON(doc).toDeepNestedProperty('definitions[1].fields', []); }); + it('rejects empty interface fields definition by default', () => { + expectSyntaxError('interface Hello {}').to.deep.equal({ + message: 'Syntax Error: Expected Name, found "}".', + locations: [{ line: 1, column: 18 }], + }); + }); + + it('parses empty fields definition with experimentalAllowEmptyInterfaceTypes', () => { + const doc = parse('interface Hello {}', { + experimentalAllowEmptyInterfaceTypes: true, + }); + + expectJSON(doc).toDeepEqual({ + kind: 'Document', + definitions: [ + { + kind: 'InterfaceTypeDefinition', + name: nameNode('Hello', { start: 10, end: 15 }), + description: undefined, + interfaces: undefined, + directives: undefined, + fields: [], + loc: { start: 0, end: 18 }, + }, + ], + loc: { start: 0, end: 18 }, + }); + }); + it('parses type with description string', () => { const doc = parse(dedent` "Description" diff --git a/src/language/parser.ts b/src/language/parser.ts index d5e398f452..80ba2c87a7 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -175,6 +175,20 @@ export interface ParseOptions { * This is equivalent to omitting the braces entirely (`type Empty`). */ experimentalAllowEmptyObjectTypes?: boolean | undefined; + + /** + * EXPERIMENTAL: + * + * If enabled, the parser accepts an empty set of braces as a fields + * definition, as proposed in https://github.com/graphql/graphql-spec/pull/1229 + * + * ```graphql + * interface Empty {} + * ``` + * + * This is equivalent to omitting the braces entirely (`interface Empty`). + */ + experimentalAllowEmptyInterfaceTypes?: boolean | undefined; } /** @@ -1218,7 +1232,8 @@ export class Parser { * FieldsDefinition : { FieldDefinition+ } * ``` * - * With `experimentalAllowEmptyObjectTypes`: + * With `experimentalAllowEmptyObjectTypes` or + * `experimentalAllowEmptyInterfaceTypes`: * * ``` * FieldsDefinition : { FieldDefinition* } @@ -1228,7 +1243,8 @@ export class Parser { */ parseFieldsDefinition(): Array | undefined { if ( - this._options.experimentalAllowEmptyObjectTypes === true && + (this._options.experimentalAllowEmptyObjectTypes === true || + this._options.experimentalAllowEmptyInterfaceTypes === true) && this.peek(TokenKind.BRACE_L) ) { return this.any( diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 8a6256b8c7..d2fd20f29e 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -664,6 +664,86 @@ describe('Type System: Objects must have fields', () => { ]); }); + it('accepts an Interface type with no fields when experimentalAllowEmptyInterfaceTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyInterface + other: OtherEmptyInterface + } + + interface EmptyInterface + + interface OtherEmptyInterface {} + + type SomeObject implements EmptyInterface & OtherEmptyInterface { + field: String + } + `, + { experimentalAllowEmptyInterfaceTypes: true }, + ); + expect(schema.experimentalAllowEmptyInterfaceTypes).to.equal(true); + expectJSON(validateSchema(schema)).toDeepEqual([]); + + const manualSchema = schemaWithFieldType( + new GraphQLInterfaceType({ name: 'EmptyInterface', fields: {} }), + ); + expectJSON(validateSchema(manualSchema)).toDeepEqual([ + { message: 'Type EmptyInterface must define one or more fields.' }, + ]); + const manualSchemaWithFlag = new GraphQLSchema({ + ...manualSchema.toConfig(), + experimentalAllowEmptyInterfaceTypes: true, + }); + expectJSON(validateSchema(manualSchemaWithFlag)).toDeepEqual([]); + + const extendedSchema = extendSchema( + schema, + parse('interface AnotherEmptyInterface'), + ); + expect(extendedSchema.experimentalAllowEmptyInterfaceTypes).to.equal(true); + expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); + }); + + it('accepts an Interface implementing an empty Interface when experimentalAllowEmptyInterfaceTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyInterface + } + + interface EmptyInterface + + interface EmptyChildInterface implements EmptyInterface + + type SomeObject implements EmptyChildInterface & EmptyInterface { + field: String + } + `, + { experimentalAllowEmptyInterfaceTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('rejects an Object type with no fields when only experimentalAllowEmptyInterfaceTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyObject + } + + type EmptyObject {} + `, + { experimentalAllowEmptyInterfaceTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: 'Type EmptyObject must define one or more fields.', + locations: [{ line: 6, column: 7 }], + }, + ]); + }); + it('rejects an Object type with incorrectly named fields', () => { const schema = schemaWithFieldType( new GraphQLObjectType({ diff --git a/src/type/schema.ts b/src/type/schema.ts index 34249ba7a8..649b894a6a 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -231,6 +231,8 @@ export class GraphQLSchema { assumeValid: boolean; /** Whether object types are allowed to define no fields. */ experimentalAllowEmptyObjectTypes: boolean; + /** Whether interface types are allowed to define no fields. */ + experimentalAllowEmptyInterfaceTypes: boolean; /** * Cached schema validation errors, if validation has already run. * @private @@ -349,6 +351,8 @@ export class GraphQLSchema { this.__validationErrors = config.assumeValid === true ? [] : undefined; this.experimentalAllowEmptyObjectTypes = config.experimentalAllowEmptyObjectTypes ?? false; + this.experimentalAllowEmptyInterfaceTypes = + config.experimentalAllowEmptyInterfaceTypes ?? false; this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -891,6 +895,8 @@ export class GraphQLSchema { extensionASTNodes: this.extensionASTNodes, assumeValid: this.assumeValid, experimentalAllowEmptyObjectTypes: this.experimentalAllowEmptyObjectTypes, + experimentalAllowEmptyInterfaceTypes: + this.experimentalAllowEmptyInterfaceTypes, }; } } @@ -916,11 +922,22 @@ export interface GraphQLSchemaValidationOptions { * If enabled, object types are allowed to define no fields, as proposed in * https://github.com/graphql/graphql-spec/pull/1228 * - * Interface types and input object types must still define one or more fields. + * Interface types (unless `experimentalAllowEmptyInterfaceTypes` is enabled) + * and input object types must still define one or more fields. * * Default: false */ experimentalAllowEmptyObjectTypes?: boolean | undefined; + + /** + * EXPERIMENTAL: + * + * If enabled, interface types are allowed to define no fields, as proposed in + * https://github.com/graphql/graphql-spec/pull/1229 + * + * Default: false + */ + experimentalAllowEmptyInterfaceTypes?: boolean | undefined; } /** Configuration used to construct a GraphQLSchema. */ @@ -954,6 +971,7 @@ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { extensionASTNodes: ReadonlyArray; assumeValid: boolean; experimentalAllowEmptyObjectTypes: boolean; + experimentalAllowEmptyInterfaceTypes: boolean; } function collectReferencedTypes( diff --git a/src/type/validate.ts b/src/type/validate.ts index 6db90f7b94..37deb81a8c 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -477,11 +477,11 @@ function validateFields( const fields = Object.values(type.getFields()); // Objects and Interfaces both must define one or more fields, unless empty - // object types are explicitly allowed. - if ( - fields.length === 0 && - !(isObjectType(type) && context.schema.experimentalAllowEmptyObjectTypes) - ) { + // object or interface types are explicitly allowed. + const allowEmpty = isObjectType(type) + ? context.schema.experimentalAllowEmptyObjectTypes + : context.schema.experimentalAllowEmptyInterfaceTypes; + if (fields.length === 0 && !allowEmpty) { context.reportError(`Type ${type} must define one or more fields.`, [ type.astNode, ...type.extensionASTNodes, diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index ba75d34b71..64f19f6e16 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -93,6 +93,39 @@ describe('Type System: build schema from introspection', () => { ).to.deep.equal({ data: { empty: { __typename: 'Empty' } } }); }); + it('builds a schema with an empty interface type', () => { + const sdl = dedent` + type Query { + empty: Empty + } + + interface Empty + + type Impl implements Empty { + field: String + } + `; + const serverSchema = buildSchema(sdl, { + experimentalAllowEmptyInterfaceTypes: true, + }); + const introspection = introspectionFromSchema(serverSchema); + const clientSchema = buildClientSchema(introspection, { + experimentalAllowEmptyInterfaceTypes: true, + }); + + expect(clientSchema.experimentalAllowEmptyInterfaceTypes).to.equal(true); + expect(printSchema(clientSchema)).to.equal(sdl); + expect( + graphqlSync({ + schema: clientSchema, + source: '{ empty { __typename ... on Impl { field } } }', + rootValue: { empty: { __typename: 'Impl', field: 'value' } }, + }), + ).to.deep.equal({ + data: { empty: { __typename: 'Impl', field: 'value' } }, + }); + }); + it('builds a schema without the query type', () => { const sdl = dedent` type Query { diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index bc91ddefd2..36e32d335a 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -76,6 +76,7 @@ export function buildASTSchema( extensionASTNodes: [], assumeValid: false, experimentalAllowEmptyObjectTypes: false, + experimentalAllowEmptyInterfaceTypes: false, }; const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); @@ -159,5 +160,7 @@ export function buildSchema( assumeValid: options?.assumeValid, experimentalAllowEmptyObjectTypes: options?.experimentalAllowEmptyObjectTypes, + experimentalAllowEmptyInterfaceTypes: + options?.experimentalAllowEmptyInterfaceTypes, }); } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index c60555d6bf..b08ce7ec87 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -145,6 +145,8 @@ export function buildClientSchema( assumeValid: options?.assumeValid, experimentalAllowEmptyObjectTypes: options?.experimentalAllowEmptyObjectTypes, + experimentalAllowEmptyInterfaceTypes: + options?.experimentalAllowEmptyInterfaceTypes, }); // Given a type reference in introspection, return the GraphQLType instance. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 82e8c11ba7..8863ee2ddf 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -304,6 +304,9 @@ export function extendSchemaImpl( experimentalAllowEmptyObjectTypes: options?.experimentalAllowEmptyObjectTypes ?? config.experimentalAllowEmptyObjectTypes, + experimentalAllowEmptyInterfaceTypes: + options?.experimentalAllowEmptyInterfaceTypes ?? + config.experimentalAllowEmptyInterfaceTypes, }; }, [SchemaElementKind.INPUT_OBJECT]: (config) => { From 802e625757d6e4d10152dd3c4e3ba155accdd559 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 20 Oct 2026 11:04:43 +0200 Subject: [PATCH 3/5] Add support for empty unions (cherry picked from commit 98e569b98e51089fe1ab99c440f91cf77ddd189c) --- src/language/__tests__/schema-parser-test.ts | 4 +- src/language/parser.ts | 6 +-- src/type/__tests__/validation-test.ts | 46 +++++++++++++++---- src/type/schema.ts | 21 +++++---- src/type/validate.ts | 7 ++- .../__tests__/buildClientSchema-test.ts | 6 +-- src/utilities/buildASTSchema.ts | 6 +-- src/utilities/buildClientSchema.ts | 4 +- src/utilities/extendSchema.ts | 6 +-- 9 files changed, 69 insertions(+), 37 deletions(-) diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts index 4255b91837..fbfdea226a 100644 --- a/src/language/__tests__/schema-parser-test.ts +++ b/src/language/__tests__/schema-parser-test.ts @@ -155,9 +155,9 @@ describe('Schema Parser', () => { }); }); - it('parses empty fields definition with experimentalAllowEmptyInterfaceTypes', () => { + it('parses empty fields definition with experimentalAllowEmptyAbstractTypes', () => { const doc = parse('interface Hello {}', { - experimentalAllowEmptyInterfaceTypes: true, + experimentalAllowEmptyAbstractTypes: true, }); expectJSON(doc).toDeepEqual({ diff --git a/src/language/parser.ts b/src/language/parser.ts index 80ba2c87a7..530c52fba3 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -188,7 +188,7 @@ export interface ParseOptions { * * This is equivalent to omitting the braces entirely (`interface Empty`). */ - experimentalAllowEmptyInterfaceTypes?: boolean | undefined; + experimentalAllowEmptyAbstractTypes?: boolean | undefined; } /** @@ -1233,7 +1233,7 @@ export class Parser { * ``` * * With `experimentalAllowEmptyObjectTypes` or - * `experimentalAllowEmptyInterfaceTypes`: + * `experimentalAllowEmptyAbstractTypes`: * * ``` * FieldsDefinition : { FieldDefinition* } @@ -1244,7 +1244,7 @@ export class Parser { parseFieldsDefinition(): Array | undefined { if ( (this._options.experimentalAllowEmptyObjectTypes === true || - this._options.experimentalAllowEmptyInterfaceTypes === true) && + this._options.experimentalAllowEmptyAbstractTypes === true) && this.peek(TokenKind.BRACE_L) ) { return this.any( diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index d2fd20f29e..2c7dc38a81 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -664,7 +664,7 @@ describe('Type System: Objects must have fields', () => { ]); }); - it('accepts an Interface type with no fields when experimentalAllowEmptyInterfaceTypes is enabled', () => { + it('accepts an Interface type with no fields when experimentalAllowEmptyAbstractTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -680,9 +680,9 @@ describe('Type System: Objects must have fields', () => { field: String } `, - { experimentalAllowEmptyInterfaceTypes: true }, + { experimentalAllowEmptyAbstractTypes: true }, ); - expect(schema.experimentalAllowEmptyInterfaceTypes).to.equal(true); + expect(schema.experimentalAllowEmptyAbstractTypes).to.equal(true); expectJSON(validateSchema(schema)).toDeepEqual([]); const manualSchema = schemaWithFieldType( @@ -693,7 +693,7 @@ describe('Type System: Objects must have fields', () => { ]); const manualSchemaWithFlag = new GraphQLSchema({ ...manualSchema.toConfig(), - experimentalAllowEmptyInterfaceTypes: true, + experimentalAllowEmptyAbstractTypes: true, }); expectJSON(validateSchema(manualSchemaWithFlag)).toDeepEqual([]); @@ -701,11 +701,11 @@ describe('Type System: Objects must have fields', () => { schema, parse('interface AnotherEmptyInterface'), ); - expect(extendedSchema.experimentalAllowEmptyInterfaceTypes).to.equal(true); + expect(extendedSchema.experimentalAllowEmptyAbstractTypes).to.equal(true); expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); }); - it('accepts an Interface implementing an empty Interface when experimentalAllowEmptyInterfaceTypes is enabled', () => { + it('accepts an Interface implementing an empty Interface when experimentalAllowEmptyAbstractTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -720,12 +720,12 @@ describe('Type System: Objects must have fields', () => { field: String } `, - { experimentalAllowEmptyInterfaceTypes: true }, + { experimentalAllowEmptyAbstractTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('rejects an Object type with no fields when only experimentalAllowEmptyInterfaceTypes is enabled', () => { + it('rejects an Object type with no fields when only experimentalAllowEmptyAbstractTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -734,7 +734,7 @@ describe('Type System: Objects must have fields', () => { type EmptyObject {} `, - { experimentalAllowEmptyInterfaceTypes: true }, + { experimentalAllowEmptyAbstractTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([ { @@ -744,6 +744,34 @@ describe('Type System: Objects must have fields', () => { ]); }); + it('accepts a Union type with no members when experimentalAllowEmptyAbstractTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + test: EmptyUnion + } + + union EmptyUnion + `, + { experimentalAllowEmptyAbstractTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([]); + + const manualSchema = schemaWithFieldType( + new GraphQLUnionType({ name: 'EmptyUnion', types: [] }), + ); + expectJSON(validateSchema(manualSchema)).toDeepEqual([ + { + message: 'Union type EmptyUnion must define one or more member types.', + }, + ]); + const manualSchemaWithFlag = new GraphQLSchema({ + ...manualSchema.toConfig(), + experimentalAllowEmptyAbstractTypes: true, + }); + expectJSON(validateSchema(manualSchemaWithFlag)).toDeepEqual([]); + }); + it('rejects an Object type with incorrectly named fields', () => { const schema = schemaWithFieldType( new GraphQLObjectType({ diff --git a/src/type/schema.ts b/src/type/schema.ts index 649b894a6a..d752688a06 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -231,8 +231,8 @@ export class GraphQLSchema { assumeValid: boolean; /** Whether object types are allowed to define no fields. */ experimentalAllowEmptyObjectTypes: boolean; - /** Whether interface types are allowed to define no fields. */ - experimentalAllowEmptyInterfaceTypes: boolean; + /** Whether interface types may define no fields and union types no members. */ + experimentalAllowEmptyAbstractTypes: boolean; /** * Cached schema validation errors, if validation has already run. * @private @@ -351,8 +351,8 @@ export class GraphQLSchema { this.__validationErrors = config.assumeValid === true ? [] : undefined; this.experimentalAllowEmptyObjectTypes = config.experimentalAllowEmptyObjectTypes ?? false; - this.experimentalAllowEmptyInterfaceTypes = - config.experimentalAllowEmptyInterfaceTypes ?? false; + this.experimentalAllowEmptyAbstractTypes = + config.experimentalAllowEmptyAbstractTypes ?? false; this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -895,8 +895,8 @@ export class GraphQLSchema { extensionASTNodes: this.extensionASTNodes, assumeValid: this.assumeValid, experimentalAllowEmptyObjectTypes: this.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyInterfaceTypes: - this.experimentalAllowEmptyInterfaceTypes, + experimentalAllowEmptyAbstractTypes: + this.experimentalAllowEmptyAbstractTypes, }; } } @@ -922,7 +922,7 @@ export interface GraphQLSchemaValidationOptions { * If enabled, object types are allowed to define no fields, as proposed in * https://github.com/graphql/graphql-spec/pull/1228 * - * Interface types (unless `experimentalAllowEmptyInterfaceTypes` is enabled) + * Interface types (unless `experimentalAllowEmptyAbstractTypes` is enabled) * and input object types must still define one or more fields. * * Default: false @@ -932,12 +932,13 @@ export interface GraphQLSchemaValidationOptions { /** * EXPERIMENTAL: * - * If enabled, interface types are allowed to define no fields, as proposed in + * If enabled, interface types are allowed to define no fields and union types + * are allowed to define no member types, as proposed in * https://github.com/graphql/graphql-spec/pull/1229 * * Default: false */ - experimentalAllowEmptyInterfaceTypes?: boolean | undefined; + experimentalAllowEmptyAbstractTypes?: boolean | undefined; } /** Configuration used to construct a GraphQLSchema. */ @@ -971,7 +972,7 @@ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { extensionASTNodes: ReadonlyArray; assumeValid: boolean; experimentalAllowEmptyObjectTypes: boolean; - experimentalAllowEmptyInterfaceTypes: boolean; + experimentalAllowEmptyAbstractTypes: boolean; } function collectReferencedTypes( diff --git a/src/type/validate.ts b/src/type/validate.ts index 37deb81a8c..d9b14650f0 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -480,7 +480,7 @@ function validateFields( // object or interface types are explicitly allowed. const allowEmpty = isObjectType(type) ? context.schema.experimentalAllowEmptyObjectTypes - : context.schema.experimentalAllowEmptyInterfaceTypes; + : context.schema.experimentalAllowEmptyAbstractTypes; if (fields.length === 0 && !allowEmpty) { context.reportError(`Type ${type} must define one or more fields.`, [ type.astNode, @@ -679,7 +679,10 @@ function validateUnionMembers( ): void { const memberTypes = union.getTypes(); - if (memberTypes.length === 0) { + if ( + memberTypes.length === 0 && + !context.schema.experimentalAllowEmptyAbstractTypes + ) { context.reportError( `Union type ${union} must define one or more member types.`, [union.astNode, ...union.extensionASTNodes], diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 64f19f6e16..8a491ceab8 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -106,14 +106,14 @@ describe('Type System: build schema from introspection', () => { } `; const serverSchema = buildSchema(sdl, { - experimentalAllowEmptyInterfaceTypes: true, + experimentalAllowEmptyAbstractTypes: true, }); const introspection = introspectionFromSchema(serverSchema); const clientSchema = buildClientSchema(introspection, { - experimentalAllowEmptyInterfaceTypes: true, + experimentalAllowEmptyAbstractTypes: true, }); - expect(clientSchema.experimentalAllowEmptyInterfaceTypes).to.equal(true); + expect(clientSchema.experimentalAllowEmptyAbstractTypes).to.equal(true); expect(printSchema(clientSchema)).to.equal(sdl); expect( graphqlSync({ diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index 36e32d335a..b93d7880a2 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -76,7 +76,7 @@ export function buildASTSchema( extensionASTNodes: [], assumeValid: false, experimentalAllowEmptyObjectTypes: false, - experimentalAllowEmptyInterfaceTypes: false, + experimentalAllowEmptyAbstractTypes: false, }; const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); @@ -160,7 +160,7 @@ export function buildSchema( assumeValid: options?.assumeValid, experimentalAllowEmptyObjectTypes: options?.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyInterfaceTypes: - options?.experimentalAllowEmptyInterfaceTypes, + experimentalAllowEmptyAbstractTypes: + options?.experimentalAllowEmptyAbstractTypes, }); } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index b08ce7ec87..8fe148a207 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -145,8 +145,8 @@ export function buildClientSchema( assumeValid: options?.assumeValid, experimentalAllowEmptyObjectTypes: options?.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyInterfaceTypes: - options?.experimentalAllowEmptyInterfaceTypes, + experimentalAllowEmptyAbstractTypes: + options?.experimentalAllowEmptyAbstractTypes, }); // Given a type reference in introspection, return the GraphQLType instance. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 8863ee2ddf..2851cada3c 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -304,9 +304,9 @@ export function extendSchemaImpl( experimentalAllowEmptyObjectTypes: options?.experimentalAllowEmptyObjectTypes ?? config.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyInterfaceTypes: - options?.experimentalAllowEmptyInterfaceTypes ?? - config.experimentalAllowEmptyInterfaceTypes, + experimentalAllowEmptyAbstractTypes: + options?.experimentalAllowEmptyAbstractTypes ?? + config.experimentalAllowEmptyAbstractTypes, }; }, [SchemaElementKind.INPUT_OBJECT]: (config) => { From 21dcfa71faf9bdb6035a275bbbb8fc3db61afb28 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:28:29 -0400 Subject: [PATCH 4/5] feat: add support for empty input object types (cherry picked from commit 2e29868a7bc48377819ef175e65d9301bbee49fa) --- src/language/__tests__/schema-parser-test.ts | 36 +++++++++ src/language/parser.ts | 30 +++++++ src/type/__tests__/validation-test.ts | 79 +++++++++++++++++++ src/type/schema.ts | 20 +++++ src/type/validate.ts | 10 ++- .../__tests__/buildClientSchema-test.ts | 27 +++++++ src/utilities/buildASTSchema.ts | 3 + src/utilities/buildClientSchema.ts | 2 + src/utilities/extendSchema.ts | 3 + 9 files changed, 209 insertions(+), 1 deletion(-) diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts index fbfdea226a..c712e5b8c7 100644 --- a/src/language/__tests__/schema-parser-test.ts +++ b/src/language/__tests__/schema-parser-test.ts @@ -177,6 +177,42 @@ describe('Schema Parser', () => { }); }); + it('rejects empty input fields definition by default', () => { + expectSyntaxError('input Hello {}').to.deep.equal({ + message: 'Syntax Error: Expected Name, found "}".', + locations: [{ line: 1, column: 14 }], + }); + }); + + it('parses empty input fields definition with experimentalAllowEmptyInputObjectTypes', () => { + const doc = parse('input Hello {}', { + experimentalAllowEmptyInputObjectTypes: true, + }); + + expectJSON(doc).toDeepEqual({ + kind: 'Document', + definitions: [ + { + kind: 'InputObjectTypeDefinition', + name: nameNode('Hello', { start: 6, end: 11 }), + description: undefined, + directives: undefined, + fields: [], + loc: { start: 0, end: 14 }, + }, + ], + loc: { start: 0, end: 14 }, + }); + }); + + it('parses empty input fields definition on extensions with experimentalAllowEmptyInputObjectTypes', () => { + const doc = parse('extend input Hello {}', { + experimentalAllowEmptyInputObjectTypes: true, + }); + + expectJSON(doc).toDeepNestedProperty('definitions[0].fields', []); + }); + it('parses type with description string', () => { const doc = parse(dedent` "Description" diff --git a/src/language/parser.ts b/src/language/parser.ts index 530c52fba3..dcd6438ee7 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -120,6 +120,20 @@ export interface ParseOptions { */ maxSourceLength?: number | undefined; + /** + * EXPERIMENTAL: + * + * If enabled, the parser accepts an empty set of braces as an input fields + * definition, as proposed in https://github.com/graphql/graphql-spec/pull/1249 + * + * ```graphql + * input Empty {} + * ``` + * + * This is equivalent to omitting the braces entirely (`input Empty`). + */ + experimentalAllowEmptyInputObjectTypes?: boolean | undefined; + /** * EXPERIMENTAL: * @@ -1487,9 +1501,25 @@ export class Parser { * InputFieldsDefinition : { InputValueDefinition+ } * ``` * + * With `experimentalAllowEmptyInputObjectTypes`: + * + * ``` + * InputFieldsDefinition : { InputValueDefinition* } + * ``` + * * @internal */ parseInputFieldsDefinition(): Array | undefined { + if ( + this._options.experimentalAllowEmptyInputObjectTypes === true && + this.peek(TokenKind.BRACE_L) + ) { + return this.any( + TokenKind.BRACE_L, + this.parseInputValueDef, + TokenKind.BRACE_R, + ); + } return this.optionalMany( TokenKind.BRACE_L, this.parseInputValueDef, diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 2c7dc38a81..8ffc8a1be4 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -1073,6 +1073,85 @@ describe('Type System: Input Objects must have fields', () => { ]); }); + it('accepts an Input Object type with no fields when experimentalAllowEmptyInputObjectTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject + `, + { experimentalAllowEmptyInputObjectTypes: true }, + ); + expect(schema.experimentalAllowEmptyInputObjectTypes).to.equal(true); + expectJSON(validateSchema(schema)).toDeepEqual([]); + + const manualSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + field: { + type: GraphQLString, + args: { + arg: { + type: new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: {}, + }), + }, + }, + }, + }, + }), + experimentalAllowEmptyInputObjectTypes: true, + }); + expectJSON(validateSchema(manualSchema)).toDeepEqual([]); + + const extendedSchema = extendSchema( + schema, + parse('input AnotherInputObject'), + ); + expect(extendedSchema.experimentalAllowEmptyInputObjectTypes).to.equal( + true, + ); + expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); + }); + + it('accepts an Input Object type with empty braces when experimentalAllowEmptyInputObjectTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject {} + `, + { experimentalAllowEmptyInputObjectTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([]); + }); + + it('rejects a OneOf Input Object type with no fields even when experimentalAllowEmptyInputObjectTypes is enabled', () => { + const schema = buildSchema( + ` + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject @oneOf + `, + { experimentalAllowEmptyInputObjectTypes: true }, + ); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'Input Object type SomeInputObject must define one or more fields.', + locations: [{ line: 6, column: 7 }], + }, + ]); + }); + it('accepts an Input Object with breakable circular reference', () => { const schema = buildSchema(` type Query { diff --git a/src/type/schema.ts b/src/type/schema.ts index d752688a06..63aff3c63c 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -233,6 +233,8 @@ export class GraphQLSchema { experimentalAllowEmptyObjectTypes: boolean; /** Whether interface types may define no fields and union types no members. */ experimentalAllowEmptyAbstractTypes: boolean; + /** Whether input object types are allowed to define no fields. */ + experimentalAllowEmptyInputObjectTypes: boolean; /** * Cached schema validation errors, if validation has already run. * @private @@ -353,6 +355,8 @@ export class GraphQLSchema { config.experimentalAllowEmptyObjectTypes ?? false; this.experimentalAllowEmptyAbstractTypes = config.experimentalAllowEmptyAbstractTypes ?? false; + this.experimentalAllowEmptyInputObjectTypes = + config.experimentalAllowEmptyInputObjectTypes ?? false; this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -897,6 +901,8 @@ export class GraphQLSchema { experimentalAllowEmptyObjectTypes: this.experimentalAllowEmptyObjectTypes, experimentalAllowEmptyAbstractTypes: this.experimentalAllowEmptyAbstractTypes, + experimentalAllowEmptyInputObjectTypes: + this.experimentalAllowEmptyInputObjectTypes, }; } } @@ -939,6 +945,19 @@ export interface GraphQLSchemaValidationOptions { * Default: false */ experimentalAllowEmptyAbstractTypes?: boolean | undefined; + + /** + * EXPERIMENTAL: + * + * If enabled, input object types are allowed to define no fields, as + * proposed in https://github.com/graphql/graphql-spec/pull/1249 + * + * OneOf input object types must still define one or more fields, because an + * empty OneOf input object can never be given a valid value. + * + * Default: false + */ + experimentalAllowEmptyInputObjectTypes?: boolean | undefined; } /** Configuration used to construct a GraphQLSchema. */ @@ -973,6 +992,7 @@ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { assumeValid: boolean; experimentalAllowEmptyObjectTypes: boolean; experimentalAllowEmptyAbstractTypes: boolean; + experimentalAllowEmptyInputObjectTypes: boolean; } function collectReferencedTypes( diff --git a/src/type/validate.ts b/src/type/validate.ts index d9b14650f0..af80f9965f 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -734,7 +734,15 @@ function validateInputFields( ): void { const fields = Object.values(inputObj.getFields()); - if (fields.length === 0) { + // Input Objects must define one or more fields, unless empty input object + // types are explicitly allowed. OneOf Input Objects must always define one or + // more fields, because an empty OneOf Input Object has no valid value. + if ( + fields.length === 0 && + !( + context.schema.experimentalAllowEmptyInputObjectTypes && !inputObj.isOneOf + ) + ) { context.reportError( `Input Object type ${inputObj} must define one or more fields.`, [inputObj.astNode, ...inputObj.extensionASTNodes], diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 8a491ceab8..fd809788f8 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -126,6 +126,33 @@ describe('Type System: build schema from introspection', () => { }); }); + it('builds a schema with an empty input object type', () => { + const sdl = dedent` + type Query { + field(arg: Empty): String + } + + input Empty + `; + const serverSchema = buildSchema(sdl, { + experimentalAllowEmptyInputObjectTypes: true, + }); + const introspection = introspectionFromSchema(serverSchema); + const clientSchema = buildClientSchema(introspection, { + experimentalAllowEmptyInputObjectTypes: true, + }); + + expect(clientSchema.experimentalAllowEmptyInputObjectTypes).to.equal(true); + expect(printSchema(clientSchema)).to.equal(sdl); + expect( + graphqlSync({ + schema: clientSchema, + source: '{ field(arg: {}) }', + rootValue: { field: 'ok' }, + }), + ).to.deep.equal({ data: { field: 'ok' } }); + }); + it('builds a schema without the query type', () => { const sdl = dedent` type Query { diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index b93d7880a2..43d1ae8f11 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -77,6 +77,7 @@ export function buildASTSchema( assumeValid: false, experimentalAllowEmptyObjectTypes: false, experimentalAllowEmptyAbstractTypes: false, + experimentalAllowEmptyInputObjectTypes: false, }; const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); @@ -162,5 +163,7 @@ export function buildSchema( options?.experimentalAllowEmptyObjectTypes, experimentalAllowEmptyAbstractTypes: options?.experimentalAllowEmptyAbstractTypes, + experimentalAllowEmptyInputObjectTypes: + options?.experimentalAllowEmptyInputObjectTypes, }); } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 8fe148a207..add8d484d5 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -147,6 +147,8 @@ export function buildClientSchema( options?.experimentalAllowEmptyObjectTypes, experimentalAllowEmptyAbstractTypes: options?.experimentalAllowEmptyAbstractTypes, + experimentalAllowEmptyInputObjectTypes: + options?.experimentalAllowEmptyInputObjectTypes, }); // Given a type reference in introspection, return the GraphQLType instance. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 2851cada3c..d87f9ccbdd 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -307,6 +307,9 @@ export function extendSchemaImpl( experimentalAllowEmptyAbstractTypes: options?.experimentalAllowEmptyAbstractTypes ?? config.experimentalAllowEmptyAbstractTypes, + experimentalAllowEmptyInputObjectTypes: + options?.experimentalAllowEmptyInputObjectTypes ?? + config.experimentalAllowEmptyInputObjectTypes, }; }, [SchemaElementKind.INPUT_OBJECT]: (config) => { From 7570e95ba5e3ca5c2dd335030c75532755831d25 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:12:20 -0400 Subject: [PATCH 5/5] refactor: merge empty type flags into one flag --- src/language/__tests__/schema-parser-test.ts | 20 ++-- src/language/parser.ts | 47 ++------- src/type/__tests__/validation-test.ts | 96 ++++++------------- src/type/schema.ts | 62 +++--------- src/type/validate.ts | 16 +--- .../__tests__/buildClientSchema-test.ts | 18 ++-- src/utilities/buildASTSchema.ts | 11 +-- src/utilities/buildClientSchema.ts | 7 +- src/utilities/extendSchema.ts | 12 +-- 9 files changed, 82 insertions(+), 207 deletions(-) diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts index c712e5b8c7..b256fa91ba 100644 --- a/src/language/__tests__/schema-parser-test.ts +++ b/src/language/__tests__/schema-parser-test.ts @@ -117,9 +117,9 @@ describe('Schema Parser', () => { }); }); - it('parses empty fields definition with experimentalAllowEmptyObjectTypes', () => { + it('parses empty fields definition with experimentalAllowEmptyTypes', () => { const doc = parse('type Hello {}', { - experimentalAllowEmptyObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(doc).toDeepEqual({ @@ -139,9 +139,9 @@ describe('Schema Parser', () => { }); }); - it('parses empty fields definition on interfaces and extensions with experimentalAllowEmptyObjectTypes', () => { + it('parses empty fields definition on interfaces and extensions with experimentalAllowEmptyTypes', () => { const doc = parse('interface Hello {} extend type World {}', { - experimentalAllowEmptyObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(doc).toDeepNestedProperty('definitions[0].fields', []); @@ -155,9 +155,9 @@ describe('Schema Parser', () => { }); }); - it('parses empty fields definition with experimentalAllowEmptyAbstractTypes', () => { + it('parses empty interface fields definition with experimentalAllowEmptyTypes', () => { const doc = parse('interface Hello {}', { - experimentalAllowEmptyAbstractTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(doc).toDeepEqual({ @@ -184,9 +184,9 @@ describe('Schema Parser', () => { }); }); - it('parses empty input fields definition with experimentalAllowEmptyInputObjectTypes', () => { + it('parses empty input fields definition with experimentalAllowEmptyTypes', () => { const doc = parse('input Hello {}', { - experimentalAllowEmptyInputObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(doc).toDeepEqual({ @@ -205,9 +205,9 @@ describe('Schema Parser', () => { }); }); - it('parses empty input fields definition on extensions with experimentalAllowEmptyInputObjectTypes', () => { + it('parses empty input fields definition on extensions with experimentalAllowEmptyTypes', () => { const doc = parse('extend input Hello {}', { - experimentalAllowEmptyInputObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(doc).toDeepNestedProperty('definitions[0].fields', []); diff --git a/src/language/parser.ts b/src/language/parser.ts index dcd6438ee7..c5a18e428a 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -120,20 +120,6 @@ export interface ParseOptions { */ maxSourceLength?: number | undefined; - /** - * EXPERIMENTAL: - * - * If enabled, the parser accepts an empty set of braces as an input fields - * definition, as proposed in https://github.com/graphql/graphql-spec/pull/1249 - * - * ```graphql - * input Empty {} - * ``` - * - * This is equivalent to omitting the braces entirely (`input Empty`). - */ - experimentalAllowEmptyInputObjectTypes?: boolean | undefined; - /** * EXPERIMENTAL: * @@ -180,29 +166,18 @@ export interface ParseOptions { * EXPERIMENTAL: * * If enabled, the parser accepts an empty set of braces as a fields - * definition, as proposed in https://github.com/graphql/graphql-spec/pull/1245 + * definition or an input fields definition, as proposed in + * https://github.com/graphql/graphql-spec/pull/1228, + * https://github.com/graphql/graphql-spec/pull/1229 and + * https://github.com/graphql/graphql-spec/pull/1249 * * ```graphql * type Empty {} - * ``` - * - * This is equivalent to omitting the braces entirely (`type Empty`). - */ - experimentalAllowEmptyObjectTypes?: boolean | undefined; - - /** - * EXPERIMENTAL: - * - * If enabled, the parser accepts an empty set of braces as a fields - * definition, as proposed in https://github.com/graphql/graphql-spec/pull/1229 - * - * ```graphql * interface Empty {} + * input Empty {} * ``` - * - * This is equivalent to omitting the braces entirely (`interface Empty`). */ - experimentalAllowEmptyAbstractTypes?: boolean | undefined; + experimentalAllowEmptyTypes?: boolean | undefined; } /** @@ -1246,8 +1221,7 @@ export class Parser { * FieldsDefinition : { FieldDefinition+ } * ``` * - * With `experimentalAllowEmptyObjectTypes` or - * `experimentalAllowEmptyAbstractTypes`: + * With `experimentalAllowEmptyTypes`: * * ``` * FieldsDefinition : { FieldDefinition* } @@ -1257,8 +1231,7 @@ export class Parser { */ parseFieldsDefinition(): Array | undefined { if ( - (this._options.experimentalAllowEmptyObjectTypes === true || - this._options.experimentalAllowEmptyAbstractTypes === true) && + this._options.experimentalAllowEmptyTypes === true && this.peek(TokenKind.BRACE_L) ) { return this.any( @@ -1501,7 +1474,7 @@ export class Parser { * InputFieldsDefinition : { InputValueDefinition+ } * ``` * - * With `experimentalAllowEmptyInputObjectTypes`: + * With `experimentalAllowEmptyTypes`: * * ``` * InputFieldsDefinition : { InputValueDefinition* } @@ -1511,7 +1484,7 @@ export class Parser { */ parseInputFieldsDefinition(): Array | undefined { if ( - this._options.experimentalAllowEmptyInputObjectTypes === true && + this._options.experimentalAllowEmptyTypes === true && this.peek(TokenKind.BRACE_L) ) { return this.any( diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 8ffc8a1be4..16b2ccc710 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -589,7 +589,7 @@ describe('Type System: Objects must have fields', () => { ]); }); - it('accepts an Object type with no fields when experimentalAllowEmptyObjectTypes is enabled', () => { + it('accepts an Object type with no fields when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -598,9 +598,9 @@ describe('Type System: Objects must have fields', () => { type EmptyObject `, - { experimentalAllowEmptyObjectTypes: true }, + { experimentalAllowEmptyTypes: true }, ); - expect(schema.experimentalAllowEmptyObjectTypes).to.equal(true); + expect(schema.experimentalAllowEmptyTypes).to.equal(true); expectJSON(validateSchema(schema)).toDeepEqual([]); const manualSchema = new GraphQLSchema({ @@ -612,7 +612,7 @@ describe('Type System: Objects must have fields', () => { }, }, }), - experimentalAllowEmptyObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(validateSchema(manualSchema)).toDeepEqual([]); @@ -620,11 +620,11 @@ describe('Type System: Objects must have fields', () => { schema, parse('type AnotherEmptyObject'), ); - expect(extendedSchema.experimentalAllowEmptyObjectTypes).to.equal(true); + expect(extendedSchema.experimentalAllowEmptyTypes).to.equal(true); expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); }); - it('accepts an Object type with empty braces when experimentalAllowEmptyObjectTypes is enabled', () => { + it('accepts an Object type with empty braces when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -633,38 +633,19 @@ describe('Type System: Objects must have fields', () => { type EmptyObject {} `, - { experimentalAllowEmptyObjectTypes: true }, + { experimentalAllowEmptyTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('accepts an empty Query root type when experimentalAllowEmptyObjectTypes is enabled', () => { + it('accepts an empty Query root type when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema('type Query', { - experimentalAllowEmptyObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('rejects an Interface type with no fields even when experimentalAllowEmptyObjectTypes is enabled', () => { - const schema = buildSchema( - ` - type Query { - test: EmptyInterface - } - - interface EmptyInterface - `, - { experimentalAllowEmptyObjectTypes: true }, - ); - expectJSON(validateSchema(schema)).toDeepEqual([ - { - message: 'Type EmptyInterface must define one or more fields.', - locations: [{ line: 6, column: 7 }], - }, - ]); - }); - - it('accepts an Interface type with no fields when experimentalAllowEmptyAbstractTypes is enabled', () => { + it('accepts an Interface type with no fields when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -680,9 +661,9 @@ describe('Type System: Objects must have fields', () => { field: String } `, - { experimentalAllowEmptyAbstractTypes: true }, + { experimentalAllowEmptyTypes: true }, ); - expect(schema.experimentalAllowEmptyAbstractTypes).to.equal(true); + expect(schema.experimentalAllowEmptyTypes).to.equal(true); expectJSON(validateSchema(schema)).toDeepEqual([]); const manualSchema = schemaWithFieldType( @@ -693,7 +674,7 @@ describe('Type System: Objects must have fields', () => { ]); const manualSchemaWithFlag = new GraphQLSchema({ ...manualSchema.toConfig(), - experimentalAllowEmptyAbstractTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(validateSchema(manualSchemaWithFlag)).toDeepEqual([]); @@ -701,11 +682,11 @@ describe('Type System: Objects must have fields', () => { schema, parse('interface AnotherEmptyInterface'), ); - expect(extendedSchema.experimentalAllowEmptyAbstractTypes).to.equal(true); + expect(extendedSchema.experimentalAllowEmptyTypes).to.equal(true); expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); }); - it('accepts an Interface implementing an empty Interface when experimentalAllowEmptyAbstractTypes is enabled', () => { + it('accepts an Interface implementing an empty Interface when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -720,31 +701,12 @@ describe('Type System: Objects must have fields', () => { field: String } `, - { experimentalAllowEmptyAbstractTypes: true }, + { experimentalAllowEmptyTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('rejects an Object type with no fields when only experimentalAllowEmptyAbstractTypes is enabled', () => { - const schema = buildSchema( - ` - type Query { - test: EmptyObject - } - - type EmptyObject {} - `, - { experimentalAllowEmptyAbstractTypes: true }, - ); - expectJSON(validateSchema(schema)).toDeepEqual([ - { - message: 'Type EmptyObject must define one or more fields.', - locations: [{ line: 6, column: 7 }], - }, - ]); - }); - - it('accepts a Union type with no members when experimentalAllowEmptyAbstractTypes is enabled', () => { + it('accepts a Union type with no members when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -753,7 +715,7 @@ describe('Type System: Objects must have fields', () => { union EmptyUnion `, - { experimentalAllowEmptyAbstractTypes: true }, + { experimentalAllowEmptyTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([]); @@ -767,7 +729,7 @@ describe('Type System: Objects must have fields', () => { ]); const manualSchemaWithFlag = new GraphQLSchema({ ...manualSchema.toConfig(), - experimentalAllowEmptyAbstractTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(validateSchema(manualSchemaWithFlag)).toDeepEqual([]); }); @@ -1073,7 +1035,7 @@ describe('Type System: Input Objects must have fields', () => { ]); }); - it('accepts an Input Object type with no fields when experimentalAllowEmptyInputObjectTypes is enabled', () => { + it('accepts an Input Object type with no fields when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -1082,9 +1044,9 @@ describe('Type System: Input Objects must have fields', () => { input SomeInputObject `, - { experimentalAllowEmptyInputObjectTypes: true }, + { experimentalAllowEmptyTypes: true }, ); - expect(schema.experimentalAllowEmptyInputObjectTypes).to.equal(true); + expect(schema.experimentalAllowEmptyTypes).to.equal(true); expectJSON(validateSchema(schema)).toDeepEqual([]); const manualSchema = new GraphQLSchema({ @@ -1104,7 +1066,7 @@ describe('Type System: Input Objects must have fields', () => { }, }, }), - experimentalAllowEmptyInputObjectTypes: true, + experimentalAllowEmptyTypes: true, }); expectJSON(validateSchema(manualSchema)).toDeepEqual([]); @@ -1112,13 +1074,11 @@ describe('Type System: Input Objects must have fields', () => { schema, parse('input AnotherInputObject'), ); - expect(extendedSchema.experimentalAllowEmptyInputObjectTypes).to.equal( - true, - ); + expect(extendedSchema.experimentalAllowEmptyTypes).to.equal(true); expectJSON(validateSchema(extendedSchema)).toDeepEqual([]); }); - it('accepts an Input Object type with empty braces when experimentalAllowEmptyInputObjectTypes is enabled', () => { + it('accepts an Input Object type with empty braces when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -1127,12 +1087,12 @@ describe('Type System: Input Objects must have fields', () => { input SomeInputObject {} `, - { experimentalAllowEmptyInputObjectTypes: true }, + { experimentalAllowEmptyTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([]); }); - it('rejects a OneOf Input Object type with no fields even when experimentalAllowEmptyInputObjectTypes is enabled', () => { + it('rejects a OneOf Input Object type with no fields even when experimentalAllowEmptyTypes is enabled', () => { const schema = buildSchema( ` type Query { @@ -1141,7 +1101,7 @@ describe('Type System: Input Objects must have fields', () => { input SomeInputObject @oneOf `, - { experimentalAllowEmptyInputObjectTypes: true }, + { experimentalAllowEmptyTypes: true }, ); expectJSON(validateSchema(schema)).toDeepEqual([ { diff --git a/src/type/schema.ts b/src/type/schema.ts index 63aff3c63c..a49290ba06 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -229,12 +229,11 @@ export class GraphQLSchema { /** Whether this schema instance skips validation checks. */ assumeValid: boolean; - /** Whether object types are allowed to define no fields. */ - experimentalAllowEmptyObjectTypes: boolean; - /** Whether interface types may define no fields and union types no members. */ - experimentalAllowEmptyAbstractTypes: boolean; - /** Whether input object types are allowed to define no fields. */ - experimentalAllowEmptyInputObjectTypes: boolean; + /** + * Whether object, interface and input object types may define no fields and + * union types may define no member types. + */ + experimentalAllowEmptyTypes: boolean; /** * Cached schema validation errors, if validation has already run. * @private @@ -351,12 +350,8 @@ export class GraphQLSchema { this.assumeValid = config.assumeValid ?? false; // Used as a cache for validateSchema(). this.__validationErrors = config.assumeValid === true ? [] : undefined; - this.experimentalAllowEmptyObjectTypes = - config.experimentalAllowEmptyObjectTypes ?? false; - this.experimentalAllowEmptyAbstractTypes = - config.experimentalAllowEmptyAbstractTypes ?? false; - this.experimentalAllowEmptyInputObjectTypes = - config.experimentalAllowEmptyInputObjectTypes ?? false; + this.experimentalAllowEmptyTypes = + config.experimentalAllowEmptyTypes ?? false; this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -898,11 +893,7 @@ export class GraphQLSchema { astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, assumeValid: this.assumeValid, - experimentalAllowEmptyObjectTypes: this.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyAbstractTypes: - this.experimentalAllowEmptyAbstractTypes, - experimentalAllowEmptyInputObjectTypes: - this.experimentalAllowEmptyInputObjectTypes, + experimentalAllowEmptyTypes: this.experimentalAllowEmptyTypes, }; } } @@ -925,39 +916,18 @@ export interface GraphQLSchemaValidationOptions { /** * EXPERIMENTAL: * - * If enabled, object types are allowed to define no fields, as proposed in - * https://github.com/graphql/graphql-spec/pull/1228 - * - * Interface types (unless `experimentalAllowEmptyAbstractTypes` is enabled) - * and input object types must still define one or more fields. - * - * Default: false - */ - experimentalAllowEmptyObjectTypes?: boolean | undefined; - - /** - * EXPERIMENTAL: - * - * If enabled, interface types are allowed to define no fields and union types - * are allowed to define no member types, as proposed in - * https://github.com/graphql/graphql-spec/pull/1229 - * - * Default: false - */ - experimentalAllowEmptyAbstractTypes?: boolean | undefined; - - /** - * EXPERIMENTAL: - * - * If enabled, input object types are allowed to define no fields, as - * proposed in https://github.com/graphql/graphql-spec/pull/1249 + * If enabled, object, interface and input object types are allowed to define + * no fields, and union types are allowed to define no member types, as + * proposed in https://github.com/graphql/graphql-spec/pull/1228, + * https://github.com/graphql/graphql-spec/pull/1229 and + * https://github.com/graphql/graphql-spec/pull/1249 * * OneOf input object types must still define one or more fields, because an * empty OneOf input object can never be given a valid value. * * Default: false */ - experimentalAllowEmptyInputObjectTypes?: boolean | undefined; + experimentalAllowEmptyTypes?: boolean | undefined; } /** Configuration used to construct a GraphQLSchema. */ @@ -990,9 +960,7 @@ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { extensions: Readonly; extensionASTNodes: ReadonlyArray; assumeValid: boolean; - experimentalAllowEmptyObjectTypes: boolean; - experimentalAllowEmptyAbstractTypes: boolean; - experimentalAllowEmptyInputObjectTypes: boolean; + experimentalAllowEmptyTypes: boolean; } function collectReferencedTypes( diff --git a/src/type/validate.ts b/src/type/validate.ts index af80f9965f..252ac4782a 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -477,11 +477,8 @@ function validateFields( const fields = Object.values(type.getFields()); // Objects and Interfaces both must define one or more fields, unless empty - // object or interface types are explicitly allowed. - const allowEmpty = isObjectType(type) - ? context.schema.experimentalAllowEmptyObjectTypes - : context.schema.experimentalAllowEmptyAbstractTypes; - if (fields.length === 0 && !allowEmpty) { + // types are explicitly allowed. + if (fields.length === 0 && !context.schema.experimentalAllowEmptyTypes) { context.reportError(`Type ${type} must define one or more fields.`, [ type.astNode, ...type.extensionASTNodes, @@ -679,10 +676,7 @@ function validateUnionMembers( ): void { const memberTypes = union.getTypes(); - if ( - memberTypes.length === 0 && - !context.schema.experimentalAllowEmptyAbstractTypes - ) { + if (memberTypes.length === 0 && !context.schema.experimentalAllowEmptyTypes) { context.reportError( `Union type ${union} must define one or more member types.`, [union.astNode, ...union.extensionASTNodes], @@ -739,9 +733,7 @@ function validateInputFields( // more fields, because an empty OneOf Input Object has no valid value. if ( fields.length === 0 && - !( - context.schema.experimentalAllowEmptyInputObjectTypes && !inputObj.isOneOf - ) + !(context.schema.experimentalAllowEmptyTypes && !inputObj.isOneOf) ) { context.reportError( `Input Object type ${inputObj} must define one or more fields.`, diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index fd809788f8..97d34a68f6 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -75,14 +75,14 @@ describe('Type System: build schema from introspection', () => { type Empty `; const serverSchema = buildSchema(sdl, { - experimentalAllowEmptyObjectTypes: true, + experimentalAllowEmptyTypes: true, }); const introspection = introspectionFromSchema(serverSchema); const clientSchema = buildClientSchema(introspection, { - experimentalAllowEmptyObjectTypes: true, + experimentalAllowEmptyTypes: true, }); - expect(clientSchema.experimentalAllowEmptyObjectTypes).to.equal(true); + expect(clientSchema.experimentalAllowEmptyTypes).to.equal(true); expect(printSchema(clientSchema)).to.equal(sdl); expect( graphqlSync({ @@ -106,14 +106,14 @@ describe('Type System: build schema from introspection', () => { } `; const serverSchema = buildSchema(sdl, { - experimentalAllowEmptyAbstractTypes: true, + experimentalAllowEmptyTypes: true, }); const introspection = introspectionFromSchema(serverSchema); const clientSchema = buildClientSchema(introspection, { - experimentalAllowEmptyAbstractTypes: true, + experimentalAllowEmptyTypes: true, }); - expect(clientSchema.experimentalAllowEmptyAbstractTypes).to.equal(true); + expect(clientSchema.experimentalAllowEmptyTypes).to.equal(true); expect(printSchema(clientSchema)).to.equal(sdl); expect( graphqlSync({ @@ -135,14 +135,14 @@ describe('Type System: build schema from introspection', () => { input Empty `; const serverSchema = buildSchema(sdl, { - experimentalAllowEmptyInputObjectTypes: true, + experimentalAllowEmptyTypes: true, }); const introspection = introspectionFromSchema(serverSchema); const clientSchema = buildClientSchema(introspection, { - experimentalAllowEmptyInputObjectTypes: true, + experimentalAllowEmptyTypes: true, }); - expect(clientSchema.experimentalAllowEmptyInputObjectTypes).to.equal(true); + expect(clientSchema.experimentalAllowEmptyTypes).to.equal(true); expect(printSchema(clientSchema)).to.equal(sdl); expect( graphqlSync({ diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index 43d1ae8f11..5343939d18 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -75,9 +75,7 @@ export function buildASTSchema( extensions: Object.create(null), extensionASTNodes: [], assumeValid: false, - experimentalAllowEmptyObjectTypes: false, - experimentalAllowEmptyAbstractTypes: false, - experimentalAllowEmptyInputObjectTypes: false, + experimentalAllowEmptyTypes: false, }; const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); @@ -159,11 +157,6 @@ export function buildSchema( return buildASTSchema(document, { assumeValidSDL: options?.assumeValidSDL, assumeValid: options?.assumeValid, - experimentalAllowEmptyObjectTypes: - options?.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyAbstractTypes: - options?.experimentalAllowEmptyAbstractTypes, - experimentalAllowEmptyInputObjectTypes: - options?.experimentalAllowEmptyInputObjectTypes, + experimentalAllowEmptyTypes: options?.experimentalAllowEmptyTypes, }); } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index add8d484d5..73a0d811c5 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -143,12 +143,7 @@ export function buildClientSchema( types: [...typeMap.values()], directives, assumeValid: options?.assumeValid, - experimentalAllowEmptyObjectTypes: - options?.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyAbstractTypes: - options?.experimentalAllowEmptyAbstractTypes, - experimentalAllowEmptyInputObjectTypes: - options?.experimentalAllowEmptyInputObjectTypes, + experimentalAllowEmptyTypes: options?.experimentalAllowEmptyTypes, }); // Given a type reference in introspection, return the GraphQLType instance. diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d87f9ccbdd..7a50852bc5 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -301,15 +301,9 @@ export function extendSchemaImpl( astNode: schemaDef ?? config.astNode, extensionASTNodes: config.extensionASTNodes.concat(schemaExtensions), assumeValid: options?.assumeValid ?? false, - experimentalAllowEmptyObjectTypes: - options?.experimentalAllowEmptyObjectTypes ?? - config.experimentalAllowEmptyObjectTypes, - experimentalAllowEmptyAbstractTypes: - options?.experimentalAllowEmptyAbstractTypes ?? - config.experimentalAllowEmptyAbstractTypes, - experimentalAllowEmptyInputObjectTypes: - options?.experimentalAllowEmptyInputObjectTypes ?? - config.experimentalAllowEmptyInputObjectTypes, + experimentalAllowEmptyTypes: + options?.experimentalAllowEmptyTypes ?? + config.experimentalAllowEmptyTypes, }; }, [SchemaElementKind.INPUT_OBJECT]: (config) => {