This document is part of the TopQuadrant GraphQL Technology Pages
This document defines an easy-to-use set of GraphQL directives that can significantly improve the
value of GraphQL schemas for JSON-based data processing.
The @uri
directive can be used to state how globally unique identifiers can be
produced for JSON objects, based on field values.
The @class
directive defines subclass relationships between GraphQL types, going
beyond the limited inheritance mechanism of the current GraphQL spec.
The @shape
directive defines semantic constraints
that all JSON objects of the GraphQL type are expected to conform to.
The @display
directive specifies user interface metadata for form-building
including default values.
JSON objects delivered by GraphQL services typically represent entities from an underlying database or some object repository. However, each time someone requests information about these entities, a new JSON object is produced, and there is no reliable mechanism to ensure that these objects indeed refer to the same entity.
Let's look at an example: below are the results of various GraphQL requests:
{ "human": { "id": "HAN", "name": "Han Solo", "friends": [ { "id": "LEIA" }, { "id": "LUKE" } ] } } |
{ "human": { "id": "LUKE", "name": "Luke Skywalker", "friends": [ { "id": "HAN" } ] } } |
{ "human": { "id": "LEIA", "name": "Leia Organa", "friends": null } } |
A client that is trying to make sense of these objects has individual, disconnected JSON tree structures to begin with:
However, both the conceptual model and the underlying database is probably rather as follows:
With a combined object structure as shown above, a client consuming the JSON objects delivered by
a GraphQL service could collect data from any subsequent request and put them into the right slots.
For example, if a future request delivers height
of one of the humans above, a corresponding
field can be added to the corresponding object.
The result is a true graph structure that can hold the collective results from many individual
JSON tree structures.
We have defined GraphQL directives that we are using to derive unique identifiers.
In the image above, these unique identifiers are, for example, ex:human-HAN
.
The technology used for these identifiers is well-established from Web technologies including RDF,
namely Unique Resource Identifiers.
Above, we use abbreviated URIs:
human:HAN
is the abbreviation for http://example.org/human/HAN
using the
namespace prefix human:
The use of URIs makes it less likely that identifiers used for data from a given data source that is represented by a given GraphQL schema will clash with data from another data source. Furthermore, URIs can be cross-referenced from outside of your local data graph, leading to a potentially huge knowledge graph that can drive user applications and allow queries that go across local boundaries.
Many GraphQL objects include one (or more) ID fields that are used to identify an entity
delivered by a server.
The @uri
directive uses such ID fields to derive globally unique URIs.
All we need to do it to annotate the GraphQL schema as follows:
schema { query: Query } type Query { human (id: ID!): [Human] } type Human @uri(template: "http://example.org/human/{$id}") { id: ID! name: String! friends: [Human] }
Using the URI templates, the three JSON result objects from the introduction can be turned into a data graph that collects and merges information from multiple requests:
URI from JSON Object | Field | Value |
---|---|---|
http://example.org/human/HAN |
id |
"HAN" |
http://example.org/human/HAN |
name |
"Han Solo" |
http://example.org/human/HAN |
friends |
http://example.org/human/LEIA |
http://example.org/human/HAN |
friends |
http://example.org/human/LUKE |
http://example.org/human/LEIA |
id |
"LEIA" |
http://example.org/human/LEIA |
name |
"Leia Organa" |
http://example.org/human/LUKE |
id |
"LUKE" |
http://example.org/human/LUKE |
name |
"Luke Skywalker" |
http://example.org/human/LUKE |
friends |
http://example.org/human/HAN |
(Readers familiar with RDF will recognize that the above table is identical to how graph databases store information in so-called triples, using a subject, a predicate and an object.)
URI templates can use values of any single-valued field using the {$fieldName}
syntax,
and insert the URI-encoded string representation of the corresponding value of these fields.
Values of these fields are assumed to be present in the JSON object, and are therefore
typically used with non-nullable fields marked with the !
operator.
If such values are absent then the URI cannot be generated, and the associated JSON object
can not be added into the data graph.
URI templates may reference multiple fields, such as http://example.org/person/{$firstName}-{$lastName}
although this is probably rare.
Namespace prefixes are used to abbreviate URIs so that they do not need to be repeated over
and over again, and so that changes to URIs just need to be made in a single place.
We introduce the @prefixes
directive to define such namespace prefixes for
a GraphQL schema:
schema @prefixes( human: "http://example.org/human/", starwars: "http://starwars.com/data/ (default)" ) { query: Query } type Human @uri(template: "human:{$id}") { id: ID! ...
The notion of URIs and namespaces does not need to be limited to instances:
even GraphQL types themselves may have a URI so that a type can become an entity in the data graph.
For this purpose, the example above marks one of the namespace prefixes as the "default".
This means that all types defined in the schema will (by default) use this namespace,
and Human
gets the URI http://starwars.com/data/Human
,
abbreviated as starwars:Human
.
We took the ideas of URIs to the next level and assigned a URI to each GraphQL schema.
By doing so, we make it possible for other schemas to reference our schema, solving
a well-known GraphQL problem coined Schema Duplication.
Basically, if one schema defines a type Movie
and you have a field that
links your local Actor
type with such movies, then wouldn't it be nice to just reference
the existing Movie
type instead of repeating it over and over?
The directive @graph
can help.
In the following example, the schema gets the URI http://starwars.com/data/
and it imports (or: includes) another, more general schema about movies.
schema @graph( uri: "http://starwars.com/data/", imports: [ "http://movies.org/data/" ] ) @prefixes( starwars: "http://starwars.com/data/ (default)", movies: "http://movies.org/data/" ) ... type Actor { appearedIn: movies_Movie ... }
The concept of referencing URIs from external files is well-known from the RDF and Linked Data worlds,
where engines may even dynamically look up other schemas from the Web, using the provided URIs.
You don't need to take it that far - the URIs may just as well be aliases to local files.
In any case, given the namespace prefixes shown above, a field such as Actor.appearedIn
can now reference the Movie
type from the namespace that has the prefix movies:
.
The example above shows that in order to use such prefixes in GraphQL identifiers, you need to
use the underscore character (instead of the :
if the reference is a string).
Organizing GraphQL schemas into multiple files is key to realizing modularity and reuse potential. However, since this mechanism is not part of the standard GraphQL spec, we had to develop a pre-processor that combines the various files into one.
Now that types can have unique identifiers, the ability to reference GraphQL types from other schemas opens up some interesting possibilities as shown in the next section.
Many data models that are accessible via GraphQL APIs are in fact object-oriented, using classes that can be arranged into a subclass hierarchy. GraphQL only supports a very shallow, one-level notion of inheritance: GraphQL types can implement interfaces, but interfaces cannot inherit from each other. Furthermore, even if your GraphQL types implement an interface, you still need to repeat all field declarations. While there may be good reasons for this (simple) design (if simplicity is the primary goal), we argue that there is a wasted opportunity here to use GraphQL schemas as a much more general data modeling language.
The @class
directive presented here aligns GraphQL with a number of object-oriented
technologies and modeling languages such as RDF and SHACL.
@class
can be used to annotate GraphQL types to instruct a processor that the type represents
a class of objects, and that the class inherits fields from its superclasses.
To make best use of this directive, you need to use a GraphQL pre-processor, or compiler,
that takes the GraphQL type definitions and "flattens" them so that all fields from superclasses
are repeated in the subclasses.
The following example states that Human
and Droid
are subclasses of
Character
.
After pre-processing, all fields from the Character
base class also apply to
Human
and Droid
.
type Character @class { appearsIn: [Episode]! friends: [Character] id: ID! name: String! } type Droid @class(subClassOf: Character) { primaryFunction: String } type Human @class(subClassOf: Character) { height: Float homePlanet: String mass: Float starships: [Starship] }
The advantages of this syntax become apparent if you want to add more subclasses, such as different types of droids with additional properties. With pure GraphQL you would need to repeat all field definitions on each level of the class "hierarchy", quickly producing an unmaintainable code base.
Note that you can define multiple superclasses by using arrays as value of subClassOf
.
Now let's combine this concept with the unique identifiers from the first section. We stated that types (here: classes) can have URIs and thus be treated as data in the data graph. In the case of the data derived from the JSON delivered by the GraphQL service, we can now add the following "triples" to our data graph:
URI from JSON Object | Field | Value |
---|---|---|
human:HAN |
type |
starwars:Human |
human:LEIA |
type |
starwars:Human |
human:LUKE |
type |
starwars:Human |
The field type
is simply assumed to be present for all classes,
and is used to link an instance with its type(s).
For those familiar with RDF, this is exactly what the property rdf:type
does.
Combined with the information about the class hierarchy, a client can now make some
"inferences".
For example, if it knows that human:HAN
is a starwars:Human
then
it knows that HAN is also an instance of starwars:Character
, and whatever knowledge
or functionality that we have about characters also applies to humans.
This includes the rich semantic constraints introduced in the following section.
The GraphQL schema language intentionally defines only a very focused and simple syntax to define fields. Basically each field has a type, may be null, and may be an array. But that's all. In many cases it would be beneficial to declare additional constraints on fields. For example:
latitude
values must be between -90 and 90startDate
of an event must be before the endDate
userName
is 8 characterscountryCode
must be from a given list of string constantsSuch constraints can be used to validate whether instance data (and JSON objects) conform to certain quality checks. Constraints can also be used to consciously constrain the user input widgets, for example to offer a drop down list for the list of available country codes. Constraints are well-known to other schema languages such as XML Schema but also the RDF-based Shapes Constraint Language SHACL.
The table below is a summary of the built-in constraint parameters for @shape
directives. They intuitively map to corresponding SHACL constraints from the sh:
namespace.
The type Name
represents GraphQL names (without quotes) or String
values
that can be translated into URI using the given prefix mapping.
Thankfully, GraphQL includes an extension mechanism, directives, that can be used to define
additional constraints.
Some tool will wants to use them, other tools can just ignore them without ill side effects.
We use the directive @shape
to annotate field definitions with constraints.
The term "shape" was chose because it nicely illustrates that these are about the shape of the allowed
data, but also to relate to the Shapes Constraint Language that defines exactly equivalent constraint types.
Let's look at an example:
# A user account type User { name: String! age: Int @shape(minInclusive: 18) gender: String @shape(in: ["male", "female"]) purchases: [Purchase] @shape(maxCount: 100) } type Purchase { # The internal ID of the product productId: String! @shape(minLength: 8, pattern: "[0-9]+") }
Above, the age
of a User
must be 18 or more, its gender
must
be either "male"
or "female"
, there cannot be more than 100 purchases
and the productId
must have at least 8 numeric characters.
The table below summarizes the supported constraint types. The hyperlinks take you to their definition in SHACL, in case you need to know the formal details.
Parameter | GraphQL Type | Description |
---|---|---|
class | Name or [Name] |
All values must be instances of $class (or a subclass).
If multiple classes are specified then the values must be instances of all of them.
|
datatype | Name |
All values must be well-formed literals of the given XSD datatype, e.g. "xsd:date" .
|
minCount | Int |
There must be at least $minCount values.
|
maxCount | Int |
There must be at most $maxCount values.
|
minExclusive | as datatype |
All values must be > $minExclusive .
|
minInclusive | as datatype |
All values must be >= $minInclusive .
|
maxExclusive | as datatype |
All values must be < $maxExclusive .
|
maxInclusive | as datatype |
All values must be <= $maxInclusive .
|
minLength | Int |
All values must have a string length of at least $minLength .
|
maxLength | Int |
All values must have a string length of at most $maxLength .
|
pattern | String |
All values must match the regular expression $pattern .
|
flags | String |
Optional flags such as "i" (ignore case) for the regular
expression matching using pattern .
|
equals | Name |
The set of all values must be equal to the set of values of the field/property
$equals .
|
disjoint | Name |
The set of all values must not overlap with the set of values of the field/property
$disjoint .
|
lessThan | Name |
All values must be < any of the values of the field/property
$lessThan .
|
lessThanOrEquals | Name |
All values must be <= any of the values of the field/property
$lessThanOrEquals .
|
node | Name or [Name] |
All values must conform to the given shape(s), i.e. must not violate any of the constraints
defined by $node .
|
hasValue | as datatype |
One of the values must be $hasValue .
|
in | [ as datatype] |
All values must be from the given list of values. |
Most of the constraint types above can also be used on user-defined scalar types.
The following example defines a scalar type Latitude
, which is a floating
point number between -90 and 90:
scalar Latitude @shape(datatype: "xsd:float", minInclusive: -90, maxInclusive: 90) scalar Longitude @shape(datatype: "xsd:float", minInclusive: -180, maxInclusive: 180) type GeoPoint { lat: Latitude long: Longitude }
Other typical examples of such scalar types include zip codes, social security numbers and country codes.
Defining them for a scalar type means that they can be reused and do not need to be repeated at
each occurrence.
Furthermore, scalar types that define a datatype
shape have well-defined meaning across
implementations, while the GraphQL standard is very unspecific and can lead to questions about whether
a scalar value should become a string or number, for example.
The following constraint arguments are supported for scalar types:
datatype
,
minInclusive
, minExclusive
, maxInclusive
,
maxExclusive
, minLength
, maxLength
,
pattern
, flags
, in
.
This section introduces the @display
directive that naturally extends
GraphQL type definitions with information that is useful for user interfaces.
Imagine you have a Customer
record and would like to display it roughly as
follows:
Names: given name: Rolf-Michel family name: Massin Address: street: 9100 Oak Street zip code: 91823 country: USA
The basic elements of such a (form) layout are that fields can be organized into groups (such as "Address" above), and these groups as well as their fields are in a given order and have human-readable display labels. GraphQL already covers the ordering (fields are naturally sorted from top to bottom), so it is just a small step to make the schema significantly more useful:
type Customer { firstName: String @display(group: NamesGroup, label: "given name") lastName: String @display(group: NamesGroup, label: "family name") street: String @display(group: AddressGroup) postalCode: String @display(group: AddressGroup, label: "zip code", label_de: "Postleitzahl") country: String @display(group: AddressGroup, defaultValue: "USA") }
To define field groups, we are using the @groups
directive at the schema.
All declared groups are global (and engines can even derive URIs for them based on
the name of the arguments of the @groups
directive).
schema @groups( NamesGroup: { label: "Names" }, AddressGroup: { label: "Address" label_de: "Addresse" } ) ...
Each entry in the @groups
directive is an object that defines one or
more labels.
Use label
to specify the "default" label.
You can use properties like label_en
to define labels for specific languages,
including 5-character language codes such as label_en_AU
for Australian visitors
of your application.
Field groups are by default ordered from top to bottom, but you can also use order
to assign a numeric value for groups. That is really only ever needed if you are merging
groups from multiple GraphQL schema files.
Once a group has been defined, individual fields can reference them using
@display(group: ...)
as shown in the example above.
Fields can define display labels using @display(label: ...)
as shown in the example
above. This includes the option to define multiple, language-specific labels using
label_de
etc.
The natural order of fields for display purposes is the same as (from top to bottom) in the GraphQL schema.
If you are relying on class inheritance and your object types are stitched
together from multiple object types, then this approach does not work, and you may need to rely
on the order
argument to assign individual numeric values to each field.
Basically, assign 0 to the upper-most field, then increase numbers.
You can use floating point numbers if you want to squeeze your subclass field in between existing
fields from a superclass.
Fields can have default values, meaning that such values can be inserted into displays
and forms even if a given object does not actually declare any value for the field.
Use @display(defaultValue: ...)
as shown in the example above.
The directives described here and implemented in TopQuadrant's products can be used by any GraphQL system.
Some systems may for example only use the @display
annotations for form building,
others may also look for @shape
constraints to validate input on said input forms.
The design of the directives can also be seen as part of a more consistent design in which GraphQL is playing a more central role than just sending JSON back and forth. In this design, GraphQL lays the foundation of modeling, representing and storing data for an application.
In order to make use of data delivered by an existing (possibly 3rd party) GraphQL web service, the URI templating and namespace mechanisms can be used to turn JSON objects into RDF graphs. This means that the results of multiple JSON requests can be added into a single unified graph database instead of remaining in disconnected tree structures.
To support this vision, we have developed a translation between GraphQL Schemas and the RDF world using the Shapes Constraint Language SHACL. This basically enables the use of GraphQL schemas as RDF domain models (aka ontologies). Benefits include the ability to define complex class hierarchies with a more flexible form of inheritance, the ability to extend GraphQL schemas with semantically richer constraints and inferencing rules, and cross-linkage between multiple schemas to reuse definitions, leading to a powerful enterprise knowledge graph. Details of the translation are described on the GraphQL Schemas to RDF/SHACL page.
Closing the circle, we also made it possible to automatically generate new GraphQL schemas that expose different view points on underlying RDF data, including the ability to ask almost arbitrary queries. The mechanism that turns an RDF/SHACL data model into GraphQL schemas is described in Querying RDF Graphs with GraphQL. Since the SHACL can be auto-generated from GraphQL schemas, this approach means that users can benefit from RDF graph technology even if they are not familiar with SHACL.