GraphQL Abstract Type Filter Specification

Status: Strawman
Version: 2026-01-09

This specification aims to provide a standardized way for clients to communicate the exclusive set of types allowed in a resolver’s response when returning one or more abstract types (i.e. an Interface or Union return type).

Algorithms are provided for resolvers to enforce this contract at runtime.

In the following example, allPets will return only Cat or Dog types:

Example № 1{
  allPets(only: ["Cat", "Dog"]) {
    ... on Cat { name }
    ... on Dog { name }
  }
}

This is enforced on the server when using the @limitTypes type system directive:

Example № 2type Query {
  allPets(only: [String] @limitTypes): [Pet]
}
@matches

This document also specifies the @matches executable directive. Client tooling may implement this to let query authors avoid manually defining the allowed types (which is implicitly already defined inside the selection set of the field for which the argument the directive is applied to).

The following example is identical to the query above when compiled (either at build time, or as a runtime transformation):

Example № 3{
  allPets @matches {
    ... on Cat { name }
    ... on Dog { name }
  }
}
Use Cases

Applications may implement this specification to provide a filter for what type(s) may be returned by a resolver. Notably, the filtering happens on the server side allowing clients to receive a fixed length of results.

This may also be used a versioning scheme by applications that dynamically render different parts of a user interface mapped from the return type(s) of a resolver. Each version of the application can define the exclusive set of types it supports displaying in the user interface.

1Abstract Type Filter Argument

1.1@limitTypes

directive @limitTypes on ARGUMENT_DEFINITION

@limitTypes is a type system directive that may be applied to a field argument definition in order to express that it will define the exclusive set of types that the field is allowed to return.

The server must enforce and validate the allowed types according to this specification.

Example Usage
Example № 4type Query {
  allPets(only: [String] @limitTypes): [Pet]
}

interface Pet {
  name: String!
}

type Cat implements Pet {
  name: String!
}

type Dog implements Pet {
  name: String!
}

@limitTypes may also be applied to schema that implements the GraphQL Cursor Connections Specification:

Example № 5type Query {
  allPetsConnection(
    first: Int
    after: String
    only: [String] @limitTypes
  ): PetConnection
}

type PetConnection {
  edges: [PetEdge]
  pageInfo: PageInfo!
}

type PetEdge {
  cursor: String!
  node: Pet
}

1.2 Schema Validation

The @limitTypes directive must not appear on more than one argument on the same field.

The @limitTypes directive may only appear on an argument that accepts a (possibly non-nullable) list of (possibly non-nullable) String.

The @limitTypes directive may only appear on an field argument where the field returns either:

1.3Execution

The @limitTypes directive places requirements on the resolver used to satisfy the field. Implementers of this specification must honor these requirements.

1.3.1Coercing Allowed Types

A filter argument is a field argument which has the @limitTypes directive applied.

The input to the filter argument is a list of strings, however this must be made meaningful to the resolver such that it may perform its filtering – thus we must resolve it into a list of valid concrete object types that are possible in this position.

The coerced list of valid concrete object types are the allowed types.

CoerceAllowedTypes(abstractType, typeNames)
  1. Let possibleTypes be a set of the possible types of abstractType.
  2. Let allowedTypes be an empty unordered set of object types.
  3. For each typeName in typeNames:
    1. Let type be the type in the schema named typeName.
    2. If type does not exist, raise an execution error.
    3. If type is an object type:
      1. If type is a member of possibleTypes, add type to allowedTypes.
      2. Otherwise, raise an execution error.
    4. Otherwise, if type is a union type:
      1. For each concreteType in type:
        1. If concreteType is a member of possibleTypes, add concreteType to allowedTypes.
    5. Otherwise, if type is an interface type:
      1. For each concreteType that implements type:
        1. If concreteType is a member of possibleTypes, add concreteType to allowedTypes.
    6. Otherwise, raise an execution error (scalars, enums, and input types are not valid filter argument values).
  4. Return allowedTypes.
Explanatory Text

The input to the filter argument may include both concrete and abstract types. CoerceAllowedTypes expands allowed types to include the possible and valid concrete types for each abstract type.

To see why this is needed, we will expand our example schema above to include the following types:

Example № 6interface Fish {
  swimSpeed: Int!
}
  
type Goldfish implements Pet & Fish {
  name: String!
  swimSpeed: Int!
}

type Haddock implements Fish {
  swimSpeed: Int!
}

It is possible for types to implement multiple interfaces. It therefore must be possible to select concrete types of another interface in the filter argument:

Example № 7{
  allPets(only: ["Fish"]) {
    ... on Goldfish {
      swimSpeed 
    }
  }
}

The below example must fail, since Haddock does not implement the Pet interface, and is therefore not a possible return type.

Counter Example № 8{
  allPets(only: ["Haddock"]) {
    ... on Fish {
      swimSpeed
    }
  }
}

1.3.2Allowed Types Restriction

Enforcement of the allowed types is the responsibility of the resolver called in ResolveFieldValue() during the ExecuteField() algorithm.

When the field returns an abstract type, the collection is this type. When the field returns a list of an abstract type, the collection is this list. When the field returns a connection type over an abstract type, the collection is the list of abstract type the connection represents.

The resolver must apply this restriction when fetching or generating the source data to produce the collection. This is because the filtering must occur prior to applying pagination logic in order to produce the correct number of results.

When a field with a @limitTypes argument is being resolved:

  • Let limitTypesArgument be the first argument with the @limitTypes directive.
  • If no such argument exists, no further action is necessary.
  • If argumentValues does not provide a value for limitTypesArgument, no further action is necessary.
  • Let limitTypes be the value in argumentValues of limitTypesArgument.
  • If limitTypes is null, no further action is necessary.
  • Let abstractType be the abstract type the collection represents.
  • Let allowedTypes be CoerceAllowedTypes(abstractType, limitTypes).
Note The restriction must be applied before pagination arguments so that non-terminal pages in the collection get full representation – i.e. there are no gaps.

1.4Validation Algorithms

@limitTypes fields must implement the algorithms listed in the Execution section above to be spec-compliant. However, it may be impossible or extremely difficult for GraphQL servers to statically verify the correctness of the runtime and prevent non-compliant implementations.

To this end, this section specifies a set of algorithms in order for the server to validate that the filter argument value and the field response are valid.

Usage of these algorithms is optional, but highly recommended to guard against programmer error.

All algorithms in this section run either before or after ResolveFieldValue(), and must be run automatically by the server when executing fields for which the @limitTypes directive is applied,

1.4.1Filter Argument Value Validation

Each member of the filter argument value must exist in the type system and be a possible return type of the field.

For example, the query below must yield an execution error – since LochNessMonster is not a type that exists in the example schema.

Counter Example № 9{
  allPets(only: ["Cat", "Dog", "LochNessMonster"]) {
    name
  }
}

When used, this algorithm must be applied before ResolveFieldValue().

ValidateFilterArgument(filterArgumentValue)
  1. Let abstractType be the abstract type the collection represents.
  2. Let possibleTypes be a set of the possible types of abstractType.
  3. For each typeName in filterArgumentValue:
    1. Let type be the type in the schema named typeName.
    2. If type does not exist, raise an execution error.
    3. If type is an object type:
      1. If type is not a member of possibleTypes raise an execution error.
    4. Otherwise, if type is a union type:
      1. ??? todo
    5. Otherwise, if type is an interface type:
      1. ??? todo
    6. Otherwise, raise an execution error (scalars, enums, and input types are not valid filter argument values).
  4. Return allowedTypes.
Note Schema-aware clients or linting tools are encouraged to implement this validation locally.

1.4.2Field Collection Validation (wip)

For example, the following query must raise an execution error since Mouse does not appear as a value in allowedTypes

Counter Example № 10{
  allPets(only: ["Cat", "Dog"]) {
    ... on Cat { name }
    ... on Dog { name }
    ... on Mouse { name }
  }
}
implement algorithm

1.4.3Field Response Validation (wip)

if the response array of the field contains a type that did not appear in CoerceAllowedTypes(), raise an execution error

yes, if a resolver already correctly implements the “Enforcing Allowed Types” logic then this isn’t necessary – but – I think this is worth speccing out as a dedicated step because this is likely something tooling will want to be able to automatically apply to all @limitTypes’d fields as a middleware. This is to provide an extra layer of safety (otherwise we’re trusting that human implementers got it right inside the resolver)

For example, given a filter argument of ["Cat", "Dog"], the following would be invalid since allPets contains Mouse:

Counter Example № 11{
  "data": {
    "allPets": [
      { "__typename": "Cat", "name": "Tom" },
      { "__typename": "Mouse", "name": "Jerry" }
    ]
  }
}

...is this even possible? this assumes that client asks for __typename which isn’t guaranteed. https://spec.graphql.org/draft/#ResolveAbstractType() likely is not possible since this logic is intended to be run generically as a middleware – i.e after the field has completed, and the in-memory object representation has been converted into json blob (potentially without __typename)

or can we look at using __resolveType()?

2@matches Directive

2.1@matches

directive @matches(
  argument: String! = "only"
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

@matches is an executable directive that clients or code generation tools may provide in order to generate the input value for a field argument which uses @limitTypes type system directive.

Note Usage of @matches is optional, but recommended to avoid duplication of the list of allowed types.
Directive Arguments

argument: : The name of the argument to populate with the list of allowed types. Defaults to "only".

Introduce the sort flag
Example Usage

This operation expresses that the allPets field may only return types that are selected for in the field’s selection set (Cat and Dog):

Example № 12{
  allPets @matches {
    ... on Cat { name }
    ... on Dog { name }
  }
}

The result of applying the document transform would be:

Example № 13{
  allPets(only: ["Cat", "Dog"]) {
    ... on Cat { name }
    ... on Dog { name }
  }
}

@matches may also be applied in operations against schema that implements the GraphQL Cursor Connections Specification:

Example № 14{
  allPetsConnection(first: 10, after: "opaqueCursor") @matches {
    edges {
      node {
        ... on Cat { name }
        ... on Dog { name }
      }
    }
  }
}

The result of applying the transform would be:

Example № 15{
  allPetsConnection(first: 10, after: "opaqueCursor", only: ["Cat", "Dog"]) {
    edges {
      node {
        ... on Cat { name }
        ... on Dog { name }
      }
    }
  }
}

2.2Document Transform

@matches is a local-only directive. The client must transform the operation (either at build-time, or at runtime) before sending the operation to the server. @matches must not appear in an operation sent to the server.

Note The server schema does not need to define the @matches directive, since it is stripped before the operation is sent to the server.

Fields that use @matches must not already define the filter argument.

Formal Specification
CollectAllowedTypes(selectionSet)
  1. Let allowedTypes be an empty set.
  2. For each selection in selectionSet:
    1. If selection is an InlineFragment:
      1. Let typeCondition be the type condition of selection.
      2. If typeCondition exists, add typeCondition to allowedTypes.
    2. If selection is a FragmentSpread:
      1. Let fragment be the fragment definition referenced by selection.
      2. Let typeCondition be the type condition of fragment.
      3. Add typeCondition to allowedTypes.
    3. If selection is a Field and its name is "edges":
      1. Let edgesSelectionSet be the selection set of selection.
      2. For each edgeSelection in edgesSelectionSet:
        1. If edgeSelection is a Field and its name is "node":
          1. Let nodeSelectionSet be the selection set of edgeSelection.
          2. Let nodeTypes be CollectAllowedTypes(nodeSelectionSet).
          3. Add each type in nodeTypes to allowedTypes.
  3. Return allowedTypes.
TransformDocument(document)
  1. For each field in document:
    1. Let matchesDirective be the directive named "matches" applied to field.
    2. If matchesDirective does not exist, continue to the next field.
    3. Let argumentName be the argument value of the "argument" argument of matchesDirective
    4. If field has an argument named argumentName, raise an error.
    5. Let selectionSet be the selection set of field.
    6. Let allowedTypes be CollectAllowedTypes(selectionSet).
    7. Let typeNames be a list of the names of each type in allowedTypes.
    8. Add an argument named argumentName with value typeNames to field.
    9. Remove matchesDirective from field.
  2. Return document.

§Index

  1. allowed types
  2. CoerceAllowedTypes
  3. CollectAllowedTypes
  4. collection
  5. filter argument
  6. TransformDocument
  7. ValidateFilterArgument
  1. 1Abstract Type Filter Argument
    1. 1.1@limitTypes
    2. 1.2 Schema Validation
    3. 1.3Execution
      1. 1.3.1Coercing Allowed Types
      2. 1.3.2Allowed Types Restriction
    4. 1.4Validation Algorithms
      1. 1.4.1Filter Argument Value Validation
      2. 1.4.2Field Collection Validation (wip)
      3. 1.4.3Field Response Validation (wip)
  2. 2@matches Directive
    1. 2.1@matches
    2. 2.2Document Transform
  3. §Index