Skip to content

Experimental Subschema empty interface#5184

Closed
tomgasson wants to merge 2 commits intofacebook:mainfrom
tomgasson:sub-schema-empty-interface
Closed

Experimental Subschema empty interface#5184
tomgasson wants to merge 2 commits intofacebook:mainfrom
tomgasson:sub-schema-empty-interface

Conversation

@tomgasson
Copy link
Copy Markdown
Contributor

In order to work out what part of our very large schema is actually being used by our products, we've been exploring using the subschema extraction. Our intent is to identify dead / unused schema and make our breaking changes detection & test selection more fine-grained (specific to the consuming product)

We've run into a problem of producing invalid schema.

schema

type Query {
  search: Searchable
}

interface Searchable {
  title: String
  score: Float
}

type Article implements Searchable {
  title: String
  score: Float
  body: String
}

usage

graphql`
  query SearchQuery {
    search {
      ... on Article {
        body
      }
    }
  }
`;

output

type Article implements Searchable {
  body: String
}

type Query {
  search: Searchable
}

interface Searchable

which is invalid, since Searchable has no fields.

  1. We can't remove Searchable because it appears as an output type of Query.search: Searchable and changing that would change the schema
  2. We can't "fake" it with a dummy field because the Article would then need to implement that dummy field too
  3. We can't use __typename (which is effectively what happens at runtime) because __ is reserved

This PR implements a "fix": If we find empty interfaces, we force their fields existence (and hence their implementing concrete types to have those fields). This means we do have "unused fields", but only for these cases.

Not ideal, however this only occurs 12 times for us so is acceptable, and I expect it's not encountered in Meta's schema if it's not yet addressed.

Interfaces that are only reached via inline fragments on implementing
types end up with missing or zero fields in the generated sub-schema.

The snapshots capture the current (broken) behavior:
- interface_empty_without_id: `interface Searchable` has zero fields
- interface_only_inline_fragments: `interface Node` missing `createdAt`
- interface_on_object_field: `interface FeedItem` missing `title`
- interface_nested: `interface Event` missing `name`
Add a `backfill_empty_interfaces` option to UsedSchemaCollectionOptions.
When enabled, after the IR walk completes, any interface that ended up
with zero fields gets all its fields touched from the full schema.

This handles the case where an interface is referenced as a return type
(so it must exist in the sub-schema) but is only accessed via inline
fragments on implementing types (so no fields were directly selected
on it). Without this, the output contains `interface Foo` with no
fields — invalid per the GraphQL spec.

Interfaces that already have at least one field are left alone, so
this only kicks in for the truly empty case.

Enabled for experimental-regenerate-sub-schema.
@meta-cla meta-cla Bot added the CLA Signed label Feb 25, 2026
@mjmahone
Copy link
Copy Markdown
Contributor

mjmahone commented Mar 4, 2026

Ah thank you for helping with this! However, this specific PR is flipping the purpose of SchemaSet on its head. SchemaSet is meant to produce intermediate, spec-invalid subsets, which we can combine/merge and individually evaluate. Only the merge of ALL subsets must produce a valid full schema (and even whether your exposed schema should be spec-valid is IMO debatable).

We do already have some "fixing" capabilities in fix_all_types: https://github.com/facebook/relay/blob/main/compiler/crates/schema-set/src/schema_set.rs#L290

But I didn't put a ton of care into that, and we almost certainly could use a pulling-apart of what exactly we are fixing and breaking down each sub-fixing component then re-composing them into a specific "fix for this purpose" pipeline.

So there's kind of three possible paths:

  • you could update your tooling to break the spec and allow Searchable to be empty. This is what we're doing, and why we don't worry about empty interfaces.
  • you could "clean up" empty interfaces: remove the empty interfaces from the schema set, and remove all references to them. We have something like this here: https://github.com/facebook/relay/blob/main/compiler/crates/schema-set/src/set_remove_defined_references.rs#L49. This though can lead to problems where types are referenced in executable docs that are now gone from the schema.
  • Alternatively we should have a pass that translates empty interfaces into unions, if you want to make sure the SchemaSet produced outputs into a valid, spec-compliant SDL without breaking existing executable documents. This can be problematic if your interface implements another interface, but... in that case your interface shouldn't be empty unless both are empty, in which case both could be independent, overlapping unions!

@tomgasson
Copy link
Copy Markdown
Contributor Author

update your tooling to break the spec and allow Searchable to be empty. This is what we're doing, and why we don't worry about empty interfaces

I think I'm convinced to do the same. Thanks for the response and detail

@tomgasson tomgasson closed this Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants