Using Scalar Types to Improve GraphQL Schema Validation
Blog

Using Scalar Types to Improve GraphQL Schema Validation

Using Scalar Types to Improve GraphQL Schema Validation

If you’ve been using GraphQL and GraphQL schemas for a while now, you recognize the benefits of having a strongly typed interface definition for your data. This strong typing at the interface level is powerful and helpful in quickly exposing incorrect input or output data from your GraphQL endpoint. It’s also helpful in assuring for both, the schema client and the schema implementer, exactly what types to expect and potentially simplifying implementations on both sides.

For example, if your schema defines the following type:

type Person {
    phoneNumbers: [PhoneNumber!]!
}

The schema client can know that they will always receive an array, possibly empty, with no null elements. This kind of strong typing reduces or eliminates a lot of ambiguity and can simplify coding to this interface.

Similarly, if your schema has a mutation that looks like this:

input PersonInput {
    name: String
    username: String!
    password: String!
}

addPerson(person: PersonInput!): Person

The person implementing the resolver for this can be assured they will be receiving an object in the person argument and that object must contain a username and password (but might not have a name).

Currently GraphQL provides a robust schema type definition specification that enables schema designers to describe fairly complete schemas for their data. For example, out of the box, GraphQL supports the following types and rules for describing a schema:

Objects (denoted with a type or input keyword and opening/closing braces):

type Account {
}

Enumerations:

enum AccountType {
    BASIC
    PREMIUM
}

type Account {
    type: AccountType!
}

Scalars including String, Int, Float, Boolean and ID as well as a required indicator (!):

type Account {
    accountID: ID!
    balance: Float!
    lastTransactionDate: String
    yearsOpen: Int
    active: Boolean!
    accountOwnerEmailAddress: String
    webAccessURL: String!
    type: AccountType!
}

And, finally, lists or arrays of these types:

type Transaction {
    amount: Float!
    date: String!
}

type Account {
    accountID: ID!
    balance: Float!
    lastTransactionDate: String
    yearsOpen: Int
    active: Boolean!
    accountOwnerEmailAddress: String
    webAccessURL: String!
    type: AccountType!
    transactions: [Transaction!]!
    tags: [String!]
}

These basic elements provide a lot of power and flexibility in defining GraphQL schemas.

However, in my experience creating schemas, I’ve pined for some additional scalars to be even more specific about the type of certain fields. Thankfully, the GraphQL folks thought of this and enabled the definition of custom scalar types. Nice.

Why would I want additional scalar types? Let’s look at the Account type we’ve been creating:

type Account {
    accountID: ID!
    balance: Float!
    lastTransactionDate: String
    yearsOpen: Int
    active: Boolean!
    accountOwnerEmailAddress: String
    webAccessURL: String!
    type: AccountType!
    transactions: [Transaction!]!
    tags: [String!]
}

The first thing to notice is that there is some uncertainty surrounding some of these fields:

balance: Float!
lastTransactionDate: String
yearsOpen: Int
accountOwnerEmailAddress: String
webAccessURL: String!

Can the balance ever be negative? Is that lastTransactionDate really a date? What about the years the account has been open? How can we be certain that the values returned for accountOwnerEmailAddress and webAccessURL are really a valid (at least syntactically) email address and URL?

This is where more specific, custom, scalars comes in handy and it’s exactly why we created the @okgrow/graphql-scalars package. Let’s use this package in our schema:

npm install --save @okgrow/graphql-scalars

Now, in our schema:

scalar DateTime

scalar NonNegativeInt
scalar NonNegativeFloat

scalar EmailAddress
scalar URL

And in our resolver map:

import {
  DateTime,
  PositiveInt,
  PositiveFloat,
  NonNegativeFloat,
  EmailAddress,
  URL,
} from '@okgrow/graphql-scalars';

const myResolverMap = {
  DateTime,

  NonNegativeInt,
  NonNegativeFloat,

  EmailAddress,
  URL,

  ...
}

And now let’s tweak our Account type:

type Account {
    accountID: ID!
    balance: NonNegativeFloat!
    lastTransactionDate: DateTime
    yearsOpen: NonNegativeInt
    active: Boolean!
    accountOwnerEmailAddress: EmailAddress
    webAccessURL: URL!
    type: AccountType!
    transactions: [Transaction!]!
    tags: [String!]
}

In our updated type definition, some uncertainties have been removed. We now know that when we get a balance it will never be negative, the lastTransactionDate will be a date the accountOwnerEmailAddress will, at least syntactically, be a valid email address. This additional specificity makes it easier and simpler to code to this schema. We can eliminate various tests, validations and checks on the consuming side of this schema because our schema has made certain things much more explicit.

The @okgrow/graphql-scalars package is our first stab at this. We’re going slowly for two reasons: 1) we don’t want to “pollute” the type space with every possible scalar type that pops into our mind, and 2) we want to be thoughtful about how some of the more complex custom scalar types should be implemented. But stayed tuned for more from this package. In the meantime, @okgrow/graphql-scalars can help you tighten up your schema and be more explicit about some of the fields in its definition.

We'll share what we've learned, get tips and info in your inbox occasionally