Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion spec/Appendix C -- Grammar Summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ ImplementsInterfaces :
- ImplementsInterfaces & NamedType
- implements `&`? NamedType

FieldsDefinition : { FieldDefinition+ }
FieldsDefinition : { FieldDefinition\* }

FieldDefinition : Description? Name ArgumentsDefinition? : Type
Directives[Const]?
Expand Down
23 changes: 17 additions & 6 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ type.

Similarly, the {`subscription`} _root operation type_ is also optional; if it is
not provided, the service does not support subscriptions. If it is provided, it
must be an Object type.
must be an Object type and must define at least one field.

The {`query`}, {`mutation`}, and {`subscription`} root types must all be
different types if provided.
Expand Down Expand Up @@ -684,7 +684,7 @@ ImplementsInterfaces :
- ImplementsInterfaces & NamedType
- implements `&`? NamedType

FieldsDefinition : { FieldDefinition+ }
FieldsDefinition : { FieldDefinition\* }

FieldDefinition : Description? Name ArgumentsDefinition? : Type
Directives[Const]?
Expand Down Expand Up @@ -717,6 +717,18 @@ Where `name` is a field that will yield a {String} value, and `age` is a field
that will yield an {Int} value, and `picture` is a field that will yield a `Url`
value.

An Object type may define no fields. This may be written with an empty set of
braces, or by omitting the braces entirely; the following two definitions are
equivalent:

```graphql example
type Empty {}
```

```graphql example
type Empty
```
Comment on lines +720 to +730

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjie pointed out in #1249 (comment) that this declaration is incomplete so I think we should remove the second example here too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaleSeo I think both syntaxes should be valid. As a matter of facts, it's already possible to do example 2 today: https://spec.graphql.org/draft/#ObjectTypeDefinition

Screenshot 2026-10-29 at 15 48 30

type Empty is valid.

See also #598 (comment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinbonnin True, it parses today, but it fails validation because it must define one or more fields. So, the question is whether it should start passing as an empty type.

Interestingly, Lee calls interface Foo "an incompletely defined interface" in the comment you linked, which lines up with @benjie's take. I'm fine either way, but I'd like all four PRs to reach the same conclusion. Maybe we should settle this with the WG?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the question is whether it should start passing as an empty type.

My preference is that it should. I'd be very surprised if type Empty {} was valid but type Empty wasn't.

Lee calls interface Foo "an incompletely defined interface" in the comment you linked, which lines up with @benjie's take

👍 Before empty types, interface Foo was an incompletely defined interface. With empty types, it becomes an empty interface, equivalent to interface Foo {}.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah Lee explicitly doesn't want this, and though I didn't agree at first, having thought about it, I do agree.

I explicitly do not want two ways of defining an empty object. Thus, I used our existing syntax:

type User

^ User with no fields. This keeps type User {} as invalid, so there's only one way of defining an empty object type.

However, type User (no braces) has always been an extension point - an indication of things to come later. It's a placeholder, and people who, until now, use it in SDL are expecting to complete the type later via extend type.

We should keep this meaning, and there should remain exactly one way to define an empty object type. Without this, users might accidentally start putting empty types into their schema, and there's no syntactic way to tell a linter "I explicitly mean for this to be an empty type" vs "this is a placeholder I shall complete later". I wish we'd not used type User to model this, and instead used declare type User, but that ship has long sailed.

So:

type User # This is a STUB - it needs to be `extend`-ed to become complete
extend type User {} # User is now a complete type with no fields
type Empty {} # Empty is a complete type with no fields

@martinbonnin martinbonnin Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I've updated that pull request to reflect that. Let me know how that sounds and I'll update the interfaces PR symmetrically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I've asked internally if we have any evidence people use this pattern. Would be nice to have data to back this.

@benjie benjie Nov 3, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve been discussing this with Dale in Discord and effectively union is where this argument falls down. There’s no way to define a union that contains no types separately to a union “placeholder” that’s waiting for extension. We could accept that union is the odd duck here and push ahead with empty braces, or we could consistently say that empty types are effectively unadorned, as I do in the parent PR as it stands. The latter has the advantage of not being a syntax change, and thus not breaking any existing parsers. I’m leaning towards the latter, but we can discuss at the WG on Thursday.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I put this PR back to its initial state where type Empty is a shorthand for type Empty {} in case we want it. Some robots might end up producing {} by removing all the field definitions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinbonnin, to close the loop, at the WG meeting yesterday, we agreed on keeping type Empty as an incomplete placeholder and using type Empty {} for an empty type. @benjie updated #1228 to match, and I did the same for #1249 and graphql/graphql-js#4934.

Your point about tools returning {} after stripping fields fits this approach well, since {} is now valid output. The syntax this PR adds is covered by #1228, so feel free to close this PR.


A query of an object value must be made via a selection set. This selection of
fields will yield an ordered map containing exactly the subset of the object
queried, which should be represented in the order in which they were queried.
Expand Down Expand Up @@ -922,8 +934,7 @@ Objects are never valid inputs.
Object types have the potential to be invalid if incorrectly defined. This set
of rules must be adhered to by every Object type in a GraphQL schema.

1. An Object type must define one or more fields.
2. For each field of an Object type:
1. For each field of an Object type:
1. The field must have a unique name within that Object type; no two fields
may share the same name.
2. The field must not have a name which begins with the characters {"\_\_"}
Expand All @@ -941,8 +952,8 @@ of rules must be adhered to by every Object type in a GraphQL schema.
1. The `@deprecated` directive must not be applied to this argument.
5. If the argument has a default value it must be compatible with
{argumentType} as per the coercion rules for that type.
3. An object type may declare that it implements one or more unique interfaces.
4. An object type must be a super-set of all interfaces it implements:
2. An object type may declare that it implements one or more unique interfaces.
3. An object type must be a super-set of all interfaces it implements:
1. Let this object type be {objectType}.
2. For each interface declared implemented as {interfaceType},
{IsValidImplementation(objectType, interfaceType)} must be {true}.
Expand Down
Loading