DEV Community

loading...
Cover image for graphQL schemas and types

graphQL schemas and types

ajcwebdev profile image anthonyCampolo Updated on ・9 min read

1. Type system

The GraphQL query language is about selecting fields on objects. In the following query:

{
  hero {
    name
    appearsIn
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Start with a "root" object
  2. Select the hero field on that
  3. For the object returned by hero, we select the name and appearsIn fields

Because the shape of a GraphQL query closely matches the result, you can predict what the query will return without knowing that much about the server. But it's useful to have an exact description of the data we can ask for:

  • What fields can we select?
  • What kinds of objects might they return?
  • What fields are available on those sub-objects?

Every GraphQL service defines a set of types describing the set of possible data you can query on that service. When queries come in they are validated and executed against that schema.

2. Type language

GraphQL services can be written in any language.

  • Since we can't rely on a specific programming language syntax, like JavaScript, to talk about GraphQL schemas, we'll define our own simple language.
  • The GraphQL schema language (similar to the query language) allows us to talk about GraphQL schemas in a language-agnostic way.

3. Object types and fields

The most basic components of a GraphQL schema are object types, which represent a kind of object you can fetch from your service, and what fields it has.

type Character {
  name: String!
  appearsIn: [Episode!]!
}
Enter fullscreen mode Exit fullscreen mode

Character

GraphQL Object Type

  • It's a type with some fields.
  • Most of the types in your schema will be object types.

name / appearsIn

Fields on Character type

  • name and appearsIn are the only fields that can appear in any part of a GraphQL query that operates on the Character type.

String

One of the built-in scalar types

  • These are types that resolve to a single scalar object
  • Can't have sub-selections in the query.

String!

Field is non-nullable

  • The GraphQL service promises to always give you a value when you query this field.
  • In the type language, we'll represent those with an exclamation mark.

[Episode!]!

Represents an array of Episode objects

  • Since it is also non-nullable, you can always expect an array (with zero or more items) when you query the appearsIn field.
  • Since Episode! is also non-nullable, you can always expect every item of the array to be an Episode object.

4. Arguments

Every field on a GraphQL object type can have zero or more arguments, for example the length field:

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}
Enter fullscreen mode Exit fullscreen mode

All arguments are named.

  • Unlike JavaScript and Python where functions take a list of ordered arguments, all arguments in GraphQL are passed by name specifically.
  • Here, the length field has one defined argument, unit.

Arguments can be either required or optional.

  • When an argument is optional, we can define a default value
  • If the unit argument is not passed, it will be set to METER by default.

5. The Query and Mutation types

Most types in your schema will just be normal object types, but there are two types that are special within a schema:

schema {
  query: Query
  mutation: Mutation
}
Enter fullscreen mode Exit fullscreen mode

Every GraphQL service has a query type and may or may not have a mutation type.

  • These types are the same as a regular object type.
  • However, they are special because they define the entry point of every GraphQL query.

If you see a query that looks like:

query {
  hero {
    name
  }
  droid(id: "2000") {
    name
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "data": {
    "hero": {
      "name": "R2-D2"
    },
    "droid": {
      "name": "C-3PO"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That means that the GraphQL service needs to have a Query type with hero and droid fields:

type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}
Enter fullscreen mode Exit fullscreen mode

Mutations work in a similar way

  • You define fields on the Mutation type.
  • They are available as the root mutation fields you can call in your query.

It's important to remember that other than the special status of being the "entry point" into the schema, the Query and Mutation types are the same as any other GraphQL object type, and their fields work exactly the same way.

6. Scalar types

A GraphQL object type has a name and fields, but at some point those fields have to resolve to some concrete data. That's where the scalar types come in: they represent the leaves of the query.

In the following query, the name and appearsIn fields will resolve to scalar types:

{
  hero {
    name
    appearsIn
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We know this because those fields don't have any sub-fields - they are the leaves of the query.

GraphQL comes with a set of default scalar types out of the box:

  • Int: A signed 32‐bit integer.
  • Float: A signed double-precision floating-point value.
  • String: A UTF‐8 character sequence.
  • Boolean: true or false.
  • ID: The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.

In most GraphQL service implementations, there is also a way to specify custom scalar types. For example, we could define a Date type:

scalar Date
Enter fullscreen mode Exit fullscreen mode

Then it's up to our implementation to define how that type should be serialized, deserialized, and validated. For example, you could specify that the Date type should always be serialized into an integer timestamp, and your client should know to expect that format for any date fields.

7. Enumeration types

Also called Enums, enumeration types are a special kind of scalar that is restricted to a particular set of allowed values. This allows you to:

  1. Validate that any arguments of this type are one of the allowed values
  2. Communicate through the type system that a field will always be one of a finite set of values

Here's what an enum definition might look like in the GraphQL schema language:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}
Enter fullscreen mode Exit fullscreen mode

This means that wherever we use the type Episode in our schema, we expect it to be exactly one of NEWHOPE, EMPIRE, or JEDI.

Note that GraphQL service implementations in various languages will have their own language-specific way to deal with enums. In languages that support enums as a first-class citizen, the implementation might take advantage of that; in a language like JavaScript with no enum support, these values might be internally mapped to a set of integers. However, these details don't leak out to the client, which can operate entirely in terms of the string names of the enum values.

8. Lists and Non-Null

Object types, scalars, and enums are the only kinds of types you can define in GraphQL. But when you use the types in other parts of the schema, or in your query variable declarations, you can apply additional type modifiers that affect validation of those values. Let's look at an example:

type Character {
  name: String!
  appearsIn: [Episode]!
}
Enter fullscreen mode Exit fullscreen mode

Here, we're using a String type and marking it as Non-Null by adding an exclamation mark, ! after the type name. This means that our server always expects to return a non-null value for this field, and if it ends up getting a null value that will actually trigger a GraphQL execution error, letting the client know that something has gone wrong.

The Non-Null type modifier can also be used when defining arguments for a field, which will cause the GraphQL server to return a validation error if a null value is passed as that argument, whether in the GraphQL string or in the variables.

query DroidById($id: ID!) {
  droid(id: $id) {
    name
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "id": null
}
Enter fullscreen mode Exit fullscreen mode
{
  "errors": [
    {
      "message": "Variable \"$id\" of required type \"ID!\" was not provided.",
      "locations": [
        {
          "line": 1,
          "column": 17
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Lists work in a similar way: We can use a type modifier to mark a type as a List, which indicates that this field will return an array of that type. In the schema language, this is denoted by wrapping the type in square brackets, [ and ]. It works the same for arguments, where the validation step will expect an array for that value.

The Non-Null and List modifiers can be combined. For example, you can have a List of Non-Null Strings:

myField: [String!]
Enter fullscreen mode Exit fullscreen mode

This means that the list itself can be null, but it can't have any null members. For example, in JSON:

myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error
Enter fullscreen mode Exit fullscreen mode

Now, let's say we defined a Non-Null List of Strings:

myField: [String]!
Enter fullscreen mode Exit fullscreen mode

This means that the list itself cannot be null, but it can contain null values:

myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid
Enter fullscreen mode Exit fullscreen mode

You can arbitrarily nest any number of Non-Null and List modifiers, according to your needs.

9. Interfaces

Like many type systems, GraphQL supports interfaces. An Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface.

For example, you could have an interface Character that represents any character in the Star Wars trilogy:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}
Enter fullscreen mode Exit fullscreen mode

This means that any type that implements Character needs to have these exact fields, with these arguments and return types.

For example, here are some types that might implement Character:

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}
Enter fullscreen mode Exit fullscreen mode

You can see that both of these types have all of the fields from the Character interface, but also bring in extra fields, totalCredits, starships and primaryFunction, that are specific to that particular type of character.

Interfaces are useful when you want to return an object or set of objects, but those might be of several different types.

For example, note that the following query produces an error:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    primaryFunction
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "ep": "JEDI"
}
Enter fullscreen mode Exit fullscreen mode
{
  "errors": [
    {
      "message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The hero field returns the type Character, which means it might be either a Human or a Droid depending on the episode argument. In the query above, you can only ask for fields that exist on the Character interface, which doesn't include primaryFunction.

To ask for a field on a specific object type, you need to use an inline fragment:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "ep": "JEDI"
}
Enter fullscreen mode Exit fullscreen mode
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

10. Union types

Union types are very similar to interfaces, but they don't get to specify any common fields between the types.

union SearchResult = Human | Droid | Starship
Enter fullscreen mode Exit fullscreen mode

Wherever we return a SearchResult type in our schema, we might get a Human, a Droid, or a Starship. Note that members of a union type need to be concrete object types; you can't create a union type out of interfaces or other unions.

In this case, if you query a field that returns the SearchResult union type, you need to use a conditional fragment to be able to query any fields at all:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "__typename": "Human",
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The __typename field resolves to a String which lets you differentiate different data types from each other on the client.

Also, in this case, since Human and Droid share a common interface (Character), you can query their common fields in one place rather than having to repeat the same fields across multiple types:

{
  search(text: "an") {
    __typename
    ... on Character {
      name
    }
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that name is still specified on Starship because otherwise it wouldn't show up in the results given that Starship is not a Character!

11. Input types

So far, we've only talked about passing scalar values, like enums or strings, as arguments into a field. But you can also easily pass complex objects.

  • This is particularly valuable in the case of mutations, where you might want to pass in a whole object to be created.
  • In the GraphQL schema language, input types look exactly the same as regular object types, but with the keyword input instead of type:
input ReviewInput {
  stars: Int!
  commentary: String
}
Enter fullscreen mode Exit fullscreen mode

Here is how you could use the input object type in a mutation:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The fields on an input object type can themselves refer to input object types, but you can't mix input and output types in your schema. Input object types also can't have arguments on their fields.

Discussion

pic
Editor guide