Schema Coordinates
At a glance
- Identifier: SchemaCoordinates
- Stage: RFC0: Strawman
- Champion: -
- PR: -
- Related:
Timeline
- RFC document updated on 2024-02-08 by Benjie
- RFC document created on 2021-01-07 by Mark Larah
RFC: Schema Coordinates
Proposed by: Mark Larah - Yelp
Spec pull request: https://github.com/graphql/graphql-spec/pull/794
This RFC proposes formalizing "Schema Coordinates" - a human readable syntax to uniquely identify a type, field, field argument, enum value, directive or directive argument defined in a GraphQL Schema.
This should be listed as a non-normative note in the GraphQL specification to serve as an official reference for use by third party tooling.
📜 Problem Statement
GraphQL tooling and libraries may wish to refer to various components of a GraphQL schema. Use cases include documentation, metrics and logging libraries.
(Example shown from GraphiQL's documentation search tab)
There already exists a convention used by some libraries and tools for writing out fields in a unique way for such purposes. However, there is no formal specification or name for this convention.
Use cases
A GraphQL server wants to log how often each field in the schema is requested. This may be implemented by incrementing a counter by the name of the schema coordinate for each field executed in a request.
Existing implementations: Yelp (internal), Facebook (internal), Shopify (API health report)
GraphiQL and other playgrounds / documentation sites want to show a list of search results when a user searches for a type or field name. We can display a list of schema coordinates that match the search term. A schema coordinate can also be used in the hyperlink to form a permalink for documentation for a particular field.
Existing implementations: GraphiQL, Apollo Studio (see "Prior Art")
A developer may want to perform analytics on all known persisted queries - e.g. what are the most commonly used fields across all documents. Schema coordinates may be used as the index/lookup keys when storing this information in the database.
Existing implementations: Yelp (internal)
A GitHub bot may want to warn developers in a Pull Request comment whenever the schema diff contains a breaking change. Schema coordinates can be used to provide a list of which fields were broken.
Existing implementations: GraphQL Inspector (see "Prior Art")
GraphQL IDEs (e.g. GraphiQL, GraphQL Playground, Apollo Studio) may wish to display the schema definition type of a node in a query when hovering over it.
Details
Example
Schema coordinates can be used to form the left hand side of this popover.
Existing implementations: Apollo Studio (see "Prior Art")
✅ RFC Goals
- There be one, unambiguous way to write a "schema coordinate" that refers to a particular element in a GraphQL schema. (This is to avoid users having to "simplify" more complex coordinates to produce a canonical representation.)
- Schema coordinate syntax should build off of existing de-facto standards already adopted for this purpose (i.e.
Foo.bar
)- Schema coordinate syntax is open for extension in the future. We should make design choices that give us flexibility and anticipate future syntax needs (based off of discussions around this RFC).
🚫 RFC Non-goals
- This does not cover "selectors" or "wildcard" syntax - e.g.
User.*
. (See alternatives considered.)- There are no proposed GraphQL language/syntax changes
- There are no proposed GraphQL runtime changes
- Schema coordinate non-goals
🧑💻 Proposed syntax
Type
Refers to a named type (e.g. something represented by
__typename
in a GraphQL introspection call).
Type.attribute
Refers to a named attribute on the named type.
Not all types support this. For object types and interface types this is a field, for input objects this would be an input field, for enums this would be an enum value, for future GraphQL types this will relate to a related concept if they have one (e.g. for the proposed "tagged" type it would refer to the "member field").
Type.field(argName:)
Refers to a named argument on the named field of the named type.
@directive
References the given named directive
@directive(argName:)
References the named argument of the named directive.
✨ Examples
For example, consider the following schema:
directive @private(scope: String!) on FIELD
type Person {
name: String
email: String @private(scope: "loggedIn")
}
type Business {
name: String
owner: Person
}
type Query {
searchBusinesses(name: String): [Business]
}We can write the following schema coordinates:
Person
uniquely identifies the the "Person" typeBusiness
uniquely identifies the the "Business" typePerson.name
uniquely identifies the "name" field on the "Person" typeBusiness.name
uniquely identifies the "name" field on the "Business" typeBusiness.owner
uniquely identifies the "owner" field on the "Business" typeQuery.searchBusinesses
uniquely identifies the "searchBusinesses" field on the "Query" typeQuery.searchBusinesses(name:)
uniquely identifies the "name" argument on the "searchBusinesses" field on the "Query" type@private
uniquely identifies the "private" directive@private(scope:)
uniquely identifies the "scope" argument on the "private" directive🎨 Prior art
The name "schema coordinates" is inspired from GraphQL Java (4.3k stars), where "field coordinates" are already used in a similar way as described in this RFC.
GraphiQL displays schema coordinates in its documentation search tab:
GraphQL Inspector (840 stars) shows schema coordinates in its output:
Apollo Studio shows schema coordinates when hovering over fields in a query:
🥣 Document -> Schema Coordinate serialization
Use cases 3 and 5 above imply that a mapping from GraphQL query nodes to schema coordinates is performed.
For example, consider the following schema:
type Person {
name: String
}
type Business {
name: String
owner: Person
}
type Query {
searchBusiness(name: String): [Business]
}And the following query:
query {
searchBusinesses(name: "El Greco Deli") {
name
owner {
name
}
}
}From the query above, we may calculate the following list of schema coordinates:
Query.searchBusinesses
Business.name
Business.owner
Person.name
Query.searchBusinesses(name:)
is also a valid member of the output set. The serialization algorithm may optionally choose to output all permutations of field arguments used, should this be specified.A library has been written to demonstrate this mapping:
https://github.com/sharkcore/extract-schema-coordinates
.🗳️ Alternatives considered
Naming
"Schema Selectors"
"Selectors" is a term used in HTML and CSS to select parts of an HTML document.
This would be a compelling, familiar choice - however, we've decided to not support wildcard expansion in this spec. See the section Syntax Non-goals.
"type/field pairs"
This was the original working name. However, there already exists more established terminology for this concept, and we also wish to describe more than just types on fields.
"Field Coordinates"
"Field Coordinates" is already understood and used by the popular GraphQL Java project.
Feedback in the August GraphQL Working Group meeting hinted that since we're targeting also describing arguments, field coordinates might not be the right name. Hence "Schema Coordinates" is chosen instead, as a more generalized form of this.
"GraphQL Coordinates"
Similar to Field Coordinates/Schema Coordinates - however, "GraphQL Coordinates" is potentially ambiguous as to if it describes schema members, query/document members or response object members.
"Field path" / "GraphQL path"
path
exists as an attribute onGraphQLResolveInfo
.Given the following query:
query {
searchBusinesses(name: "El Greco Deli") {
name
owner {
name
}
}
}
Person.name
in the response may be written as the following "field path":["query", "searchBusinesses", 1, "owner", "name"]
Note that here, the "path" is a serialized response tree traversal, instead of describing the location of the field in the schema.
Since "path" is already used in GraphQL nomenclature to describe the location of a field in a response, we'll avoid overloading this term.
Separator
This RFC proposes using "
.
" as the separator character between a type and field. The following have also been proposed:
Foo::bar
Foo#bar
Foo->bar
Foo~bar
Foo:bar
"
.
" is already used in the existing implementations of field coordinates, hence the suggested usage in this RFC. However, we may wish to consider one of the alternatives above, should this conflict with existing or planned language features.Field Arguments
We have discussed multiple options for selecting arguments on fields. (PR, and December WG Meeting). For example, consider the following schema:
type Query {
rollDice(numDice: Int, numSides: Int): Int
}We may want to refer to the
numDice
argument in a schema selector. Two options for this syntax are:
Query.rollDice.numDice
Query.rollDice(numDice:)
Pros for
Query.rollDice.numDice
- Less bytes/characters to type
- May allow for extension to include nested "field paths" (e.g. Foo.bar.Baz.qux...)
- Prior usage of this syntax to represent state internally
Pros for
Query.rollDice(numDice:)
- Indicating arguments with colons disambiguates against other types of schema nodes. For those unfamiliar with schema selectors, it may be unclear if the third dot separated item refers to a directive or a child object etc.
- Using trailing colons for arguments is borrowed from other languages (e.g. [Swift][swift]). This may indicate to users who are unfamiliar with schema coordinates, but recognize this from other languages, that
numDice:
refers to an argument. The function parentheses and colons more strongly communicate "this is an argument!" than a second dot separator.Decision
We are choosing
Query.rollDice(numDice:)
to optimize for readability and extensibility.Given our expected use cases, we assume Schema Coordinates will be read more often than they are written (e.g. error messages in a stack trace from a schema linting tool). Readers may be unfamiliar with its syntax. We want to "hint" as much as possible the meaning of the coordinates in its syntax. We think
(numDice:)
more clearly communicates that "numDice" is an argument, over.numDice
.In addition, we want to be mindful of extensions to this syntax in the future. Using dots only as a separator may overload the meaning of elements in schema coordinates in the future. (If we capture new schema node types, or nested paths.)
We should make sure that the spec enables future innovation including using it for things other than schema coordinates. To my mind the (foo:) syntax is more flexible in this regard. For example, I can imagine referring to:
Foo.bar(baz.qux:)
: the qux field of the input object referred to from the baz argument of the bar field on the Foo type.Foo.bar(baz:).qux
: the qux field on the return type of the bar field (with baz: argument) of the Foo type.Foo.bar.baz.qux
: the qux field of the return type of the baz field on the return type of the bar field on type Foo.If we were to only use periods then all of these would come out the same as
Foo.bar.baz.qux
, and this ambiguity precludes this kind of reusal of the schema-coordinates syntax for this use case (which is outside the scope of the schema coordinates spec, for sure, but is still a potential use-case for the syntax).~ benjie
🙅 Syntax Non-goals
This syntax consciously does not cover the following use cases:
Wildcard selectors
Those familiar with
document.querySelector
may be expecting the ability to pass "wildcards" or "star syntax" to be able to select multiple schema elements. This implies multiple ways of selecting a schema node.For example,
User.address
andUser.a*
might both resolve toUser.address
. ButUser.a*
could also ambiguously refer toUser.age
.It's unclear how wildcard expansion would work with respect to field arguments*, potentially violating the requirement of this schema to uniquely identify schema components.
* (e.g. does
Query.getUser
also select all arguments on thegetUser
field? Who knows! A discussion for another time.)A more general purpose schema selector language could be built on top of this spec - however, we'll consider this out of scope for now.
Nested field paths
This spec does not support selecting schema members with a path from a root type (e.g.
Query
).For example, given this schema
type User {
name: String
bestFriend: User
}
type Query {
userById(id: String): User
}The following are invalid schema coordinates:
Query.userById.name
User.bestFriend.bestFriend.bestFriend.name
This violates a non-goal that there be one, unambiguous way to write a schema coordinate to refer to a schema member. Both examples can be "simplified" to
User.name
, which is a valid schema coordinate.Should a use case for this arise in the future, a follow up RFC may investigate how schema coordinates could work with "field paths" (e.g.
["query", "searchBusinesses", 1, "owner", "name"]
) to cover this.Directive applications
This spec does not support selecting applications of directive.
For example:
directive @private(scope: String!) on FIELD
type User {
name: String
reviewCount: Int
friends: [User]
email: String @private(scope: "loggedIn")
}You can select the definition of the
private
directive and its arguments (with@private
and@private(scope:)
respectively), but you cannot select the application of the@private
onUser.email
.For the stated use cases of this RFC, it is more likely that consumers want to select and track usage and changes to the definition of the custom directive instead.
If we did want to support this, a syntax such as
User.email@private[0]
could work. (The indexing is necessary since multiple applications of the same directive is allowed, and each is considered unique.)Union members
This spec does not support selecting members inside a union definition.
For example:
type Breakfast {
eggCount: Int
}
type Lunch {
sandwichFilling: String
}
union Meal = Breakfast | LunchYou may select the
Meal
definition (as "Meal
"), but you may not select members onMeal
(e.g.Meal.Breakfast
orMeal.Lunch
).It is unclear what the use case for this would be, so we won't (yet?) support this. In such cases, consumers may select type members directly (e.g.
Lunch
).Answered questions
Would we want to add a method to graphql-js? A
fieldCoordinateToFieldNode
method (for example) may take in a field coordinate string and return a field AST node to serve as a helper / reference implementation of the algorithm to look up the field node.Update: This was discussed in the August Working Group Meeting - it was suggested to keep any utilities as third party libraries to avoid edge ambiguity problems, and to be able to iterate faster.