GraphQL Cost Directives

Note This document is a DRAFT. It is being published to solicit feedback. It is a “live document” that can change whenever feedback is given and accepted.

This first draft of this document is © Copyright IBM Corp. 2021.

1Goals

This specification defines a set of GraphQL SDL directives that captures information useful for expressing the cost of executing GraphQL queries.

Note The Goals, Example, and Introspection sections, and all notes like this one, are non-normative and intended to provide a good understanding of the motivation for this specification, as well as hints that might benefit implementors.

The spec proposes to standardize these directives, with the following goals:

2Collaboration

The specification landing page has some short videos explaining parts of this specification, including motivation for why this work is important.

For communication, we have created a channel #cost-analysis on the GraphQL Foundation’s Slack workspace to invite anyone to openly discuss these ideas.

Alternatively, you can directly message us on Slack. This spec is largely written by Morris Matsa based on work he did for years with spec co-author Erik Wittern and through several iterations with our customers. We also have significant contributions from Igor Belyi and Andy Chang, and collaboration with Jim Laredo and Alan Cha. We hope others will join us.

3Example

An example of an SDL document containing the proposed directives is:

Example № 1type User {
  name: String
  age: Int @cost(weight: "2.0")
}

type Query {
  users(max: Int): [User] @listSize(slicingArguments: ["max"])
}

How expensive or costly are various fields? Really this depends on how costly it is to run the resolver functions on the GraphQL server. With the ‘raw’ SDL, or GraphQL schema, we don’t know, but with the added directives we know that:

We can consider how to apply this enhanced schema to the following example query:

Example № 2query Example {
  users (max: 5) {
    age
  }
}

The field cost of this query can be statically analyzed (see Static Query Analysis) as 11.0, as the query returns at most:

When this query is actually executed using the GraphQL engine, it might not have five users, so as an example suppose that it has three users and each one has an age. Then the actual cost of executing this query can be dynamically analyzed (see Dynamic Query Analysis) to be 7.0:

Besides dynamically summing up the field cost to 7.0 as we run the execution engine, we could also analyze the resulting example JSON data (see Query Response Analysis):

Example № 3{ "users" : [
      { "age" : 33 },
      { "age" : 45 },
      { "age" : 27 },
   ]
}

In this case, we can “reverse engineer” the cost to have been 7.0:

While a static analysis of the query calculates a maximum (upper-bound) cost of 11.0 unlike both a dynamic cost calculation and a post-facto static analysis of the resulting data which calculates a cost of 7.0, there is no contradiction and both numbers are interesting:

Here are a few examples of how these cost calculations can be used:

All of these advantages can be achieved across the three methods of cost analysis by adding the same simple @cost and @listSize directives to the GraphQL schema.

4Reserved Directives

A GraphQL server which conforms to this spec must reserve certain GraphQL directives and GraphQL directive names. In particular, this spec creates guidelines for the following directives:

5Cost Analysis

Cost Analysis is the process of analyzing a GraphQL transaction with the goal of quantifying the cost or expense of its execution. The process of cost analysis results in calculated counts and costs of running that query.

5.1Methods of Cost Analysis

There are different Methods of Cost Analysis:

Different methods of Cost Analysis offer different semantics of precisely what their results mean, and thus do not always produce the same count and cost values. For example, Static Query Analysis could say that ten Users might be returned, where Query Response Analysis would show that only three Users were actually returned. Alternatively, Static Query Analysis could say that an Address with a Street and City might be returned, where Dynamic Query Analysis would show that the entire Address was returned as null and the Street and City resolvers were never executed.

The three types of Cost Analysis have inherent tradeoffs, and the relative strength of their advantages depends on the environment in which GraphQL is used. Therefore, different methods will be appropriate at different times, and sometimes it can be appropriate to run multiple methods of cost analysis. One environment could run Static Cost Analysis to protect against unsafe queries and Dynamic Query Analysis to charge an API Consumer via monetization. Another environment might prefer Query Response Analysis to get a tight bound on the real cost without adding any load to the backend server. Many combinations are possible.

5.1.1Static Query Analysis

Static Query Analysis is the process of conducting a static analysis on the input GraphQL Query, with full knowledge of the GraphQL schema for the GraphQL server.

The results of Static Query Analysis should always produce an upper bound on the results calculated by Dynamic Query Analysis and Query Response Analysis, as they are based on calculating what might happen and picking the most conservative values. This is a very useful property for a-priori decisions in security-sensitive settings.

Static Query Analysis is the least accurate of the three types of Cost Analysis, but it also has the large advantage of being the only one to happen before spending compute and other resources while running the execution engine.

Static Query Analysis can be used, for example, to allow or disallow queries to be executed based on configured rate limit or threat prevention policies. It can be applied in either GraphQL backends or dedicated middleware components.

5.1.2Dynamic Query Analysis

Dynamic Query Analysis is the process of counting dynamically as the execution engine produces parts of the result data. The results are exact counts of how much data is produced. Errors do not produce data.

Note It is significant that errors are not included in count and cost data. The detection of errors should be included in any full strategy for dealing with Threat Protection, Rate Limiting, or Monetization.

Dynamic Query Analysis can be used on a GraphQL server to abort query execution when certain cost thresholds are reached for Threat Protection, or for Rate Limiting.

5.1.3Query Response Analysis

Query Response Analysis is the process of Cost Analysis running as an analysis of the result from the execution engine. This analysis uses three inputs:

  • The GraphQL schema for the GraphQL server
  • The GraphQL query that produced this result data
  • The result data itself

By combining these three input sources, the Query Response Analysis can compute a much tighter estimate of the actual counts and cost than would have been computed in Static Query Analysis.

Query Response Analysis knows exact sizes of lists returned, which is the largest unknown quantity in Static Query Analysis, however it is not always possible to know exactly which types are returned without control of the execution engine, therefore counts and costs are still an upper bound for Query Response Analysis, although in practice they are usually exact, and at least a much tighter bound.

Query Response Analysis can be used in GraphQL backends or dedicated middleware components to accurately update remaining rates towards an enforced rate limit.

5.2Inputs to Cost Analysis

All three Methods of Cost Analysis include an input of the GraphQL schema. For all of these methods, the unenhanced schema would not be sufficient to calculate the cost. Therefore, the input schema includes both the schema constructs which are part of the GraphQL specification, and additional information in the form of directives on that schema which provide:

  • Numerical Weights on various schema constructs. These are used to calculate weighted sums for the result Costs.
  • List Size configuration to aid in bounding the sizes of returned lists.

The Static Query Analysis method also uses an input of the transaction’s GraphQL query, while the Query Response Analysis uses as inputs both the GraphQL query and the GraphQL response. Dynamic Query Analysis can be embedded into the GraphQL execution engine with access to its state during execution.

5.3Results of Cost Analysis

Each of the three Methods of Cost Analysis yields two kinds of results: Counts and Costs. These two kinds of results from Cost Analysis MAY be provided as outputs of the whole Cost Analysis, or MAY be maintained only as internal data used as part of Cost Analysis. Furthermore, they may be skipped if it does not change the overall results of Cost Analysis, and may be stored in any form or calculated in any order. The purpose of describing them here is to explain a straightforward algorithm that is easy to understand and verify, without eliminating any opportunities for optimization.

For example, some users of Cost Analysis might use the Counts as output to achieve rate-limiting based on a subset of the counts, where other users of Cost Analysis might leave the Counts as an internal step towards calculating the Costs.

5.3.1Counts

Cost analysis computes Counts, a set of count values, where each value is a non-negative integer and corresponds to a particular part of the GraphQL schema.

When a given key refers to a specific parts of a GraphQL schema, we use a “GraphQL Path” notation. This notation is the same as the notation used in many parts of the industry today, and should evolve with the RFC: Schema Coordinates by Mark Larah.

5.3.1.1Type Counts

A type count corresponds to a specific type in the schema. In this case types includes all types which can appear in GraphQL responses: scalar types, object types, interface types, union types, and enum types. The count gives the number of times that a query produces something of that specific type. The various type counts MAY be combined into any hash map, such as a JSON object.

Example № 4{
   "Query" : 1,
   "User" : 3
}

5.3.1.2Input Type Counts

An input type counts corresponds to a specific input object type in the schema. Input object types are not in Type Counts because they do not appear in the GraphQL responses. The count gives the number of times that a query returns a field which uses that input type, which is the number of times that a resolver function was run for a field with this input type.

Example № 5{
   "UserInput" : 1
}

5.3.1.3Field Counts

A field count corresponds to a specific field which is defined on an object type or interface type in the schema. The count gives the number of times that a query returns that specific field on that type, which is the number of times that a resolver was run on that field.

Example № 6{
   "Query.user" : 3,
   "User.name" : 3,
   "User.friends" : 1
}

5.3.1.4Input Field Counts

An input field count corresponds to a specific field which is defined on an input object type in the schema. The count gives the number of times that a query returns a field which uses this input field, which is the number of times that a resolver function was run for a field using an input type using this input field.

Example № 7{
   "UserInput.name" : 1,
   "ProduceInput.category" : 2,
   "Filter.age" : 1
}

5.3.1.5Argument Counts

An argument count corresponds to a specific argument to either a field or a directive on the schema. The count gives the number of times that a specific argument is used in a query, on either a field or a directive, which is the number of times that a resolver function was run for a field which uses this argument.

Example № 8{
   "Query.users.limit" : 1,
   "@skip.if" : 2
}

5.3.1.6Directive Counts

A directive count corresponds to a specific directive in the schema. The count gives the number of times a specific directive is used, which is the number of times that a resolver function was run for a field which uses this directive.

Example № 9{
   "@defer" : 1,
   "@include" : 2
}

An implementation MAY store all key-value pairs of count values in a single map or it MAY store them in a number of maps, one for type counts, one for field counts, etc.

Note This document specifies how to calculate each count, but if counts are saved or presented such as being logged or persisted in a database, then it might make sense to only record counts for the most used or most costly items. Regardless, all counts must be assessed by Cost Analysis so that it can produce an accurate cost calculation.

5.3.2Costs

Cost Analysis can result in various Costs, which are numeric values characterizing the net effective expense of executing the query on the GraphQL server.

5.3.2.1Type Cost

The Type Cost is the weighted sum of the Type Counts.

5.3.2.2Field Cost

The overall Field Cost can be calculated by adding together the cost of every object field. The cost of each object field can be calculated by recursively applying these algorithms to calculate the cost of all arguments, directives, and fields, including object fields and input fields.

Arguments: For every argument a, calculate its cost:

  • Add up:
    • The weight of argument a
    • The sum of the cost of all of the input fields which are used on argument a
  • This sum may be negative

Directives: For every directive d calculate its cost:

  • Add up:
    • The weight of directive d
    • The sum of the cost of all of the arguments on directive d
  • This sum may be negative

Fields: For every field f calculate its cost:

  • First, add up the raw cost of field f by calculating the sum of:
    • The weight of field f
    • The sum of the cost of all directives on field f
    • The sum of the cost of all arguments on field f
  • Second, if this sum is negative then round it up to zero
Example: Argument Weights
Example № 10type Query {
  topProducts(filter: Filter @cost(weight: "15.0")): [String] @cost(weight: "5.0") @listSize(assumedSize: 10)
}

In this example, a query that asks for topProducts has a Field Cost of 5.0 unless it uses a filter which results in much more work in the same single resolver function and therefore increases the Field Cost on this field to 20.0, as the sum of 5.0 for the field itself and 15.0 for the argument.

Example: Negative Weights
Example № 11type Query {
  mostPopularProduct(approx: Approximate @cost(weight: "-3.0")): Product @cost(weight: "5.0")
}

In this example, a query that asks for the most popular product has a Field Cost of 5.0 unless it uses an argument to indicate that only an approximate answer is needed and not an exact transactional answer, in which case the cost would reduce to 2.0 as 5.0 - 3.0 = 2.0. This is an example of where negative weights can be used even though a given field cannot add a negative value to the overall cost. Conceptually, we can understand that certain arguments might make it less expensive to run a given resolver function.

Example: Input Field Weights
Example № 12input Filter {
  approx: Approximate @cost(weight: "-12.0")
}
type Query {
  topProducts(filter: Filter @cost(weight: "15.0")): [String] @cost(weight: "5.0") @listSize(assumedSize: 10)
}

This example builds on example above where the filter contributed an extra 15.0 to the cost of the topProducts field. Now we qualify that: If the filter is only approximate then the weight is decreased by adding -12.0 which means that the filter argument overall contributes a weight of 15.0 - 12.0 = 3.0 to the base field cost of 5.0 for an overall field cost of 8.0.

Example: Directive Arguments
Example № 13directive @approx (tolerance: Float! @cost(weight: "-1.0")) on FIELD

In this example, we modify the @approx directive’s required tolerance argument to have a weight of -1.0, which therefore adds -1.0 to the cost of every use of the @approx directive, and in turn to every field which uses the @approx directive. This slightly decreases the cost of every field in a query which only needs approximate results.

Weights used in Response

Query Response Analysis has access to the original query in addition to the response, so it knows the directives and arguments on a field and can calculate the same costs as the other methods.

6Introspection

The GraphQL specification defines introspection using GraphQL itself to create “a powerful platform for tool-building”. For any GraphQL server or middleware implementing this specification, it can optionally support extensions to introspection to further enhance this powerful platform and enable tools that assist the client with understanding the cost of their queries.

6.1Introspection of Cost Analysis Results

The results of cost analysis can be added into the introspection. As with any introspection, some servers might disable it for security or enable it to support powerful tooling. Each system MAY extend introspection with cost analysis results, and if they do then they MUST extend introspection in these ways to be compatible.

6.1.1Naming

The GraphQL specification requires that: “Types and fields required by the GraphQL introspection system that are used in the same context as user-defined types and fields are prefixed with "__" two underscores.”

This extension to introspection maintains that rule.

6.1.2__cost

This extension to the schema introspection system is accessible from the meta-field __cost which is accessible from the type of the root of a query operation.

__cost : __Cost!

Like all meta-fields, __cost is implicit and does not appear in the fields list in the root type of the query operation.

The schema of the GraphQL cost introspection system:

type __Cost {
   requestCosts : __CostMetrics
   responseCosts : __CostMetrics
}

type __CostMetrics {
   fieldCounts(regexName: String) : [__CostCountType!]!
   typeCounts(regexName: String) : [__CostCountType!]!
   inputTypeCounts(regexName: String) : [__CostCountType!]!
   inputFieldCounts(regexName: String) : [__CostCountType!]!
   argumentCounts(regexName: String) : [__CostCountType!]!
   directiveCounts(regexName: String) : [__CostCountType!]!

   fieldCost : Float!
   typeCost : Float!

   fieldCostByLocation(regexPath: String) : [__CostByLocation!]!
   typeCostByLocation(regexPath: String) : [__CostByLocation!]!
}

type __CostByLocation {
   path: String!
   cost: Float!
}

type __CostCountType {
   name: String!
   value:  Int!
}

The counts fieldCounts, typeCounts, inputTypeCounts, inputFieldCounts, argumentCounts, and directiveCounts returned correspond to the result counts calculated for the overall query.

The returned typeCost and fieldCost correspond to the overall type cost and field cost of the query, respectively.

The costs by location returned by typeCostByLocation and fieldCostByLocation are the subset of the overall typeCost and fieldCost calculated for the given subsets of the overall query.

6.1.3regexName Arguments

All regular expression name arguments regexName on fields in the __Cost type are regular expressions on the name of the relevant part of the GraphQL schema provided by the GraphQL schema type system, and that format should evolve with the RFC: Schema Coordinates by Mark Larah.

Each of these regular expressions limits the scope of responses in the returned list: When returning a list of __CostCountType objects, each __CostCountType‘s name must match the regular expression.

This regular expression always applies to the full name, as if the start string and end string qualifiers are always specified. This implies that whenever no wildcards are used, the returned list much have at most one item in it.

6.1.4regexPath Arguments

All regular expression path arguments regexPath are regular expressions on the string representing the path into a particular part of the GraphQL query. This path is a dot-separated list of fields from the root of the query, with references to directives, fragments, and arguments expressed as they would be using Schema Coordinates. There are a number of considerations necessary to make a proper “Query Coordinates” spec to match/extend the existing “Schema Coordinates” RFC. We’ve made some choices here, but if Query Coordinates get standardized as part of the GraphQL spec, then that standardization should be used here instead of these choices.

In the absence of a proper GraphQL Query Coordinates RFC, we are including some examples to illustrate the idea. We feel that an actual spec for Query Coordinates should be independent and not part of this document.

Consider this example GraphQL query:

Example № 14query exampleQuery ($VarDef1: Int @custom(first: 1)) {
  a1(arg1: 1, arg2: {f1: 3, f2: 4}) {
    x @dir1(f3: 3, f4: 4)
    ...b @dir2
    ... on A @dir3 {
      c
    }
  }
  a2 {
    ...b
    ... on A {
      nest {
        y
      }
    }
    ... on A {
      nest {
        z
      }
    }
  }
  alias1:a2 {
    ...b
  }

  a3(arg1: [{d1: 3, d2: "f"}, 2, "foo", false], arg2: $VarDef1) {
    g
  }

  a3(arg1: [{d1: 3, d2: "f"}, 2, "foo", false], arg2: $VarDef1) {
    g
  }

}
fragment b on A @dir4 {
  h
}

In this query, legal paths include:

  • exampleQuery($VarDef1:) - This is the path to the variable definition $VarDef1
  • exampleQuery($VarDef1:).@custom - This is the path to the directive custom within variable definition $VarDef1
  • exampleQuery($VarDef1:).@custom(first:) - This is the path to the argument first of the directive custom
  • exampleQuery.a1 - This is the path to the first field of the query
  • exampleQuery.a1(arg1:) - This is the path to the a1 field’s arg1 argument
  • exampleQuery.a1(arg2:).f2 - This is the path to the a1 field’s arg1 argument’s f2 input field
  • exampleQuery.a1.x - This is the path to the a1 field’s selection set’s x field
  • exampleQuery.a1.~b - This is the path to the b fragment spread within the a1 field
  • exampleQuery.a1.on~A - This is the path to the on A fragment spread
  • exampleQuery.a1.on~A.@dir3 - This is the path to that fragment spread’s directive
  • exampleQuery.a1.on~A.c - This is the path to that fragment spread’s c field
  • exampleQuery.a2.on~A[0] - This is the path to the first ... on A within a2
  • exampleQuery.a2.on~A[1] - This is the path to the second ... on A within a2
  • exampleQuery.alias1 - This is the path to the field a2 aliased alias1
  • exampleQuery.alias1.~b - This is the path to the fragment spread b within the field a2aliased alias1
  • exampleQuery.a3[0](arg1:)[2] - This is the path to the third value in list argument arg1
  • exampleQuery.a3[0](arg1:)[0].d1 - This is the path to the d1 field in the first value in list argument arg1
  • exampleQuery.a3[1].g - This is the path to the g field within the second a3 field
  • ~~b - This is the path to the fragment b, ie. the definition of the b fragment
  • ~~b.@dir4 - This is the path to the directive on that fragment definition
  • ~~b.h - This is the path to the field h of the fragment definition b

Take note that there are two ... on A inlined fragments with the a2 field. It makes sense for end-user tooling to want to display the contribution of each of these two to the overall query cost independently. Therefore, we need to be able to have unique paths that distinguish between them. In this case we add [0] and [1] to the end of the path. There could be later path elements as well, for example:

  • a2.on~A[0].nest - The first nest field
  • a2.on~A[1].nest - The second nest field

To keep the path expressions simple:

  • For any part of the path with only a single occurance in the query, the [0] must be ommitted.
  • For any regular expression, it should take into account that any path element might or might not have an index.

We hope that a proper GraphQL Query Coordinates RFC will improve this situation.

Here is another example:

Example № 15
query A {
  a {
    y: x { i j }
    x: y
    z: x { j k }
    ...B
 }
}

fragment B {
  z
}

query B {
  a {
    a
    b
    ...A
 }
}

fragment A {
  c
}

In this query, legal paths include:

  • A - This is the path to the A operation
  • A.a - This is the path to that operation’s a field
  • A.a.y - This refers to the y: x field, using the alias
  • A.a.x - This refers to the x: y field, using the alias
  • ~~B - This is a path to the fragment definiton of B
  • B - This is a path to the operation definition of B

When returning a list of __CostByLocation objects, each __CostByLocation‘s path must match the regular expression.

Just like the regexName arguments, this regular expression always applies to the full path, as if the start string and end string qualifiers are always specified. This implies that whenever no wildcards are used, the returned list much have at most one item in it.

6.1.5requestCosts and responseCosts

Implementations which extend introspection with cost results must return at least one of requestCosts and responseCosts as non-null. If returning a non-null requestCosts it should have been computed by Static Query Analysis. If returning a non-null responseCosts it should have been computed by either Dynamic Query Analysis or Query Response Analysis.

6.2introspectionOnly

Implementations may support an HTTP Header named introspectionOnly. If this header is passed, then that HTTP transaction MUST only execute the introspection parts of the query, including __schema, __type, __cost, and any other extensions to introspection.

This header gives the client tooling the ability:

  • to pass this header and a query which will be analyzed and yield the results of cost analysis, without actually executing the non-meta part of the query.
  • to not pass this header, thus defaulting to executing all parts of the query including the meta and non-meta parts.

6.3NoSchemaIntrospectionCustomRule

Since __cost is an extension of introspection, any custom rule that turns off all introspection such as the graphql-js v15.2.0 feature, must also forbid use of __cost.

Implementations may make finer-grained setting to only forbid parts of introspection, however a generic toggle or validation rule must apply to all parts of introspection equally.

6.4Introspection of Cost Directives

This document specifies adding cost directives to a GraphQL schema. It would be useful for client tooling to automatically introspect these directives from a GraphQL server, allowing the server to advertise how expensive various GraphQL constructs are in that GraphQL schema. It would help if GraphQL introspection results had an option to include directives in the GraphQL schema, as is being discussed as a possible change to the GraphQL specification.

7The Cost Directive

The purpose of the cost directive is to define a weight for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response.

Formally, the definition of the cost Directive is:

directive @cost(weight: String!) on 
  | ARGUMENT_DEFINITION
  | ENUM
  | FIELD_DEFINITION
  | INPUT_FIELD_DEFINITION
  | OBJECT
  | SCALAR

7.1weight

The weight argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc. Being of type non-null String, the weight argument has to be set for every use of the @cost directive. In other words, it is the sole purpose of the cost directive to set a weight. While weight is required on @cost, it is not required to include the @cost directive on any schema construct.

While the type of weight is a String!, the value of the String may be a serialized Float. The reason that the definition is String is to allow extensibility of using a formula which computes a Float. All implementations must accept a literal serialized Float.

When a @cost directive is not provided, Cost Analysis relies on default weights:

  • For scalar and enum types the weight defaults to "0.0".
  • Fields returning scalar and enum types, arguments of scalar and enum types, as well as input fields of scalar and enum types all default to "0.0".
  • Weights for all composite input and output types default to "1.0".

These default weights reflect the fact that scalar and enum values are often contained in their composite parents, like a “name” being contained in a “user” object, so while they require an additional GraphQL resolver function to be called, it usually does not result in an additional call outside of the GraphQL execution engine.

Weights can be negative. The overall calculated weight of a field or directive can never be negative, and if found to be negative must be rounded up to zero, but negative weights on arguments can be useful when combined with positive weights on the field that uses those arguments. See Negative Weights.

7.2Locations

The @cost directive can be used in a variety of locations, namely on all elements relevant for calculating different kinds of cost.

Deliberately, the @cost directive cannot be used on abstract types, neither interfaces nor unions. For such types, Cost Analysis should consider as weight the maximum weight of any of the member types. This constraint is required to ensure that Static Cost Analysis on a query produces cost values that are upper bounds of the values produced by either Dynamic Query Analysis or Query Response Analysis, which run after a concrete type was determined during execution.

For the same reason, the cost directive cannot be used on fields of an interface.

Additonally, the @cost directive cannot be used on an input object, although it can be used on the fields within an input object. The rationale is that the input object itself adds to the size of the query which can be calculated exactly without any directives, whereas the fields in the input type might correspond to additonal work needed in the resolver function for an object’s field, and should affect the weight of that other field.

8The List Size Directive

A main challenge when statically analyzing queries is how to produce counts and cost for fields that return lists of values. During query execution, those fields’ resolver functions return lists of finite size, limited either by the sparsity of data, by limits hard-coded in the GraphQL server implementation (e.g., how many entries to select in a database query), or relying on the values of slicing arguments used for pagination and sent in queries.

Without access to the implementation of a GraphQL server or its underlying data, a static query analysis cannot know the size of lists. To be conservative and not under-estimate cost and counts, it must initially assume the length of lists to be infinite. This approach does guarantee that the analysis produces cost and count upper bounds, but it renders the resulting cost and counts to be meaningless.

The purpose of the @listSize directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information.

Formally, the definition of the listSize Directive is:

directive @listSize(
  assumedSize: Int,
  slicingArguments: [String!],
  sizedFields: [String!],
  requireOneSlicingArgument: Boolean = true
) on FIELD_DEFINITION

8.1assumedSize

The assumedSize argument can be used to statically define the maximum length of a list returned by a field.

For example, in the following schema document, the assumedSize argument states that the list of strings returned by field Query.topProducts will always be 10:

Example № 16type Query {
  topProducts: [String] @listSize(assumedSize: 10)
}

For static analysis to produce accurate counts and cost, the GraphQL server must ensure that the list returned by a field with an assumedSize directive does in fact return lists of (at most) the defined size.

8.2slicingArguments

The slicingArguments argument can be used to define which of the field’s arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments.

For example, in the following schema document, the slicingArguments argument states that the max argument of field Query.users is a slicing argument:

Example № 17type Query {
  users (max: Int!): [String] @listSize(slicingArguments: ["max"])
}

When analyzing a query that includes that field, the static analysis can use the value of the max argument to determine how long the list of users is going to be (at most).

For any argument with a default value in the schema but no value given in the query, static analysis should treat it as if that value was given in the query.

When multiple slicing arguments are defined and a query contains more than one, static analysis should consider their largest value to ensure producing upper bounds.

8.3sizedFields

The sizedFields argument can be used to define that the value of the assumedSize argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields.

This option is relevant primarily for schemas that implement the GraphQL Cursor Connections Specification. In it, fields with slicing arguments first and last return a single Connection with a field edges that itself returns the sliced list (in which each element is contained in an Edge).

For example, in the following schema document, the field Query.films has two slicing arguments first and last. The field returns a single FilmConnection, which itself provides access to multiple FilmEdges via its FilmConnection.edges field. The sizedFields option states the the size of the list returned by FilmConnection.edges is determined by the value of one of the slicing arguments of the parent field:

Example № 18type FilmEdge {
  cursor: ID
  node: Film # Definition of this type left out in this example
}

type FilmConnection {
  edges: [FilmEdge]
  # See https://relay.dev/graphql/connections.htm#sec-Connection-Types.Fields.PageInfo
  pageInfo: PageInfo
}

type Query {
  films(first: Int, after: ID, last: Int, before: ID): FilmConnection @listSize(
      slicingArguments: ["first", "last"],
      sizedFields: ["edges"]
    )
}

8.4requireOneSlicingArgument

The requireOneSlicingArgument argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error.

Requiring a single slicing argument is useful once more for schemas that implement the GraphQL Cursor Connections Specification. They define two slicing arguments, first and last, for paginated fields. GraphQL does not support stating that exactly one or the other is required. In consequence, GraphQL API providers like GitHub implement custom validation rules to make sure exactly one slicing argument is used in a query. The requireOneSlicingArgument argument allows to configure this behavior.

If no slicingArguments are defined, then the value of requireOneSlicingArgument is ignored.

Per default, requireOneSlicingArgument is enabled, and has to be explicitly disabled if not desired for a field.

8.5Locations

The listSize directive can be used solely on field definitions.

9Validation

The directives defined by this spec can be validated on two dimensions: First, that their definitions are present in an SDL, and are adhering to this spec. Second, that the use of directives within the SDL is correct.

9.1Cost Directive

An SDL document that implements this spec should contain a directive definition for the cost directive. Specifically:

  • The cost directive should define a single argument named weight of type non-null String.
  • The cost should be non-repeatable.
  • The cost directive should define the locations ARGUMENT_DEFINITION, ENUM, FIELD_DEFINITION, INPUT_FIELD_DEFINITION, OBJECT, and SCALAR.

The correct usage of the cost directive is partly covered by GraphQL’s specified validation rules. Specifically:

In addition, further rules for using the cost directive should be validated.

9.1.1No Cost on Interface Fields

The cost directive is not allowed on fields in an interface. The cost of a field on an interface can be calculated based on the costs of the corresponding field on each concrete type implementing that interface, either directly or indirectly through other interfaces.

9.2List Size Directive

An SDL document that implements this spec should contain a directive definition for the listSize directive. Specifically:

  • The listSize directive should define the following arguments:
    • An argument named assumedSize with type Int.
    • An argument named slicingArguments with type list of String.
    • An argument named sizedFields with type list of String.
    • An argument named requireOneSlicingArgument with type Boolean and a default value of true.
  • The listSize should be non-repeatable.
  • The listSize directive should define the single location FIELD_DEFINITION.

The correct usage of the listSize directive is partly covered by GraphQL’s specified validation rules. Specifically:

In addition, further rules for using the listSize directive should be validated.

9.2.1Valid List Size Target

The listSize directive should only be used on fields that return lists, or it uses the sizedFields argument.

9.2.2Valid Sized Fields Target

Using the sizedFields argument in a listSize directive requires that both:

  • The named sized fields are defined in the return type of the annotated field.
  • The named sized field return lists.

9.2.3Valid Slicing Arguments Target

Using the slicingArguments argument in a listSize directive requires that both:

  • The named slicing arguments are defined in the annotated field.
  • The named slicing arguments have a type non-null Int or simply Int.

9.2.4Valid Assumed Size

Using the assumedSize argument in a listSize directive requires that:

  • Either the same directive does not also use the slicingArguments argument, or:
  • The same directive does use the slicingArguments argument, but none of the slicing arguments has a default value, and the requireOneSlicingArgument is set to false.

The rationale behind this rule is to avoid cases where it is ambiguous for static analysis whether to use the value of the assumedSize argument or that of a slicing argument. The second condition renders the value of the assumedSize argument a backup for queries that do not use a slicing argument, and where no slicing argument has a default value. In cases where a slicing argument is provided on a field that also has an assumedSize defined, static analysis should prioritize the value of the slicing argument.

§Index

  1. Cost Analysis
  2. cost Directive
  3. Counts
  4. Dynamic Query Analysis
  5. Field Cost
  6. listSize Directive
  7. Methods of Cost Analysis
  8. Query Response Analysis
  9. Static Query Analysis
  10. Type Cost
  1. 1Goals
  2. 2Collaboration
  3. 3Example
  4. 4Reserved Directives
  5. 5Cost Analysis
    1. 5.1Methods of Cost Analysis
      1. 5.1.1Static Query Analysis
      2. 5.1.2Dynamic Query Analysis
      3. 5.1.3Query Response Analysis
    2. 5.2Inputs to Cost Analysis
    3. 5.3Results of Cost Analysis
      1. 5.3.1Counts
        1. 5.3.1.1Type Counts
        2. 5.3.1.2Input Type Counts
        3. 5.3.1.3Field Counts
        4. 5.3.1.4Input Field Counts
        5. 5.3.1.5Argument Counts
        6. 5.3.1.6Directive Counts
      2. 5.3.2Costs
        1. 5.3.2.1Type Cost
        2. 5.3.2.2Field Cost
  6. 6Introspection
    1. 6.1Introspection of Cost Analysis Results
      1. 6.1.1Naming
      2. 6.1.2__cost
      3. 6.1.3regexName Arguments
      4. 6.1.4regexPath Arguments
      5. 6.1.5requestCosts and responseCosts
    2. 6.2introspectionOnly
    3. 6.3NoSchemaIntrospectionCustomRule
    4. 6.4Introspection of Cost Directives
  7. 7The Cost Directive
    1. 7.1weight
    2. 7.2Locations
  8. 8The List Size Directive
    1. 8.1assumedSize
    2. 8.2slicingArguments
    3. 8.3sizedFields
    4. 8.4requireOneSlicingArgument
    5. 8.5Locations
  9. 9Validation
    1. 9.1Cost Directive
      1. 9.1.1No Cost on Interface Fields
    2. 9.2List Size Directive
      1. 9.2.1Valid List Size Target
      2. 9.2.2Valid Sized Fields Target
      3. 9.2.3Valid Slicing Arguments Target
      4. 9.2.4Valid Assumed Size
  10. §Index