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.