GraphQL Scalar でハッピーライフを
はじめに
TypeScript で GraphQL の開発をしていた時に「GraphQL は String しか扱えないから Date 型そのまま渡せなくて不便だな...。」と思っていました。
そんな過去の自分へ Scalar type と Custom Scalar を理解すると、圧倒的に GraphQL は効率的になるということを伝えるために筆を取りました。
TL;DR
GraphQL の値のバリデーション、Serialize は極力 Scalar type でやると良い。
Build-in Scalar で要件を満たさない場合は、Custom Scalar を利用する。
https://github.com/Urigo/graphql-scalars というライブラリには、汎用的な Custom Scalar が定義されていて非常に便利。
Scalar type とは
Scalar type とは GraphQL における最もプリミティブな型です。
GraphQL で出力される値は、Scalar type か Enum か null のどれかになります。
type User {
name: String!
email: String!
age: Int!
}
Build-in でサポートされている Scalar type は 5 種類あります。
それぞれ
- どんな値を入力できるか (Input Coercion)
- どんな値を出力として持つか (Result Coercion)
- Json として出力する際に行う値の変換方法 (Serialization)
が定義されています。
5 種類についてどのように設定されているか確認すると
Scalar type | Input Coercion | Result Coercion | Serialization |
---|---|---|---|
Int | 整数値 | 整数値とみなせる値 ("123" なども含む) | Number へ変換 |
Float | 整数値もしくは浮動小数値 | 浮動小数値とみなせる値 ("123" なども含む) | Number へ変換 |
String | Unicode 文字 | Unicode 文字 | String へ変換 |
Boolean | Boolean (true, 1, "1" など) | Boolean | true もしくは false へ変換 |
ID | 文字列もしくは整数値 | ID とみなせる値 | String へ変換 |
となります。
これらは GraphQL の仕様書に記載されているものとなり、実際のライブラリの実装は実装者に任されています。
Build-in Scalar を使っているときに感じた課題
少し話が戻りますが、
「GraphQL は String しか扱えないから Date 型そのまま渡せなくて不便だな...。」
と思っていました。「不便」を分解すると、いくつかのポイントがあります。
全部 String に変換しないといけない
Build-in の Scalar type のみで表現しようとすると以下のようになってしまいます。
type User {
archivedAt: String!
}
JavaScript の Date 型を渡したいと思ってコードを書いてもエラーになってしまいます。
// OK
return {
archivedAt: (new Date()).toISOString()
}
// NG
return {
archivedAt: new Date()
}
これは色々なところで toString()
を書かなければならなくて、面倒です。
バリデーションを中央集権的にするのが難しい
さらに問題なのは、フロントから String を渡されることを許容してしまうことです。
本来であれば欲しいデータは日付のフォーマット化された文字列なのに、"test" みたいな文字列を許容してしまいます。
# 本当はエラーにしたいリクエスト
query {
userByArchivedAt(gtArchivedAt: "test") {
name
}
}
つまりフロントエンドでバックエンドが想定していない文字列を送っても、 GraphQL の Scalar type という仕組みでは許可してしまうという問題が発生します。
この問題に対応するために、バリデーションをそれぞれの Query や Mutation で書かなければならなくなってしまいます。困りました。
Custom Scalar をどう使うか
そこで、GraphQL では Custom Scalar という仕様が用意されています。
GraphQL Schema に Custom Scalar を宣言する
Schema に以下のように自分で定義した Scalar を宣言することができます。
scalar Email @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc5322")
@specifiedBy
はディレクティブと呼ばれるもので、この Scalar の定義が書かれたドキュメントのリンクを指定します。多くの GUI GraphQL クライアントで値を参照できるようになっており、Scalar はどんな値なのかを知るのに役立ちます。
Resolver を実装する
また、前述の通り Scalar は Input Coercion、 Result Coercion、 Serialization の 3 つの機能を持つ必要があります。
TypeScript の GraphQL 実装 https://github.com/graphql の場合、これらは以下のように実装することができます。
// EmailType という Resolver を作成し、GraphQL server の resolver に追加する
const EmailType = new GraphQLScalarType({
name: "Email",
serialize: isEmail,
parseValue: isEmail,
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return isEmail(ast.value)
}
throw new Error("The input value is not email formated.")
},
})
function isEmail(value) {
if (isEmail(value)) {
return value
}
throw new Error("The input value is not email formated.")
}
parseValue
および parseLiteral
は両方とも Input Coercion として利用されます。
ここで parseValue
は、GraphQL の input variables として値が渡された時に利用されます。
input UserInput {
email: Email!
}
type Query {
userByEmail(variables: UserInput!): User!
}
一方で parseLiteral
は、literal values として値が渡された時に利用されます。
type Query {
userByEmail(email: Email!): User!
}
そして serialize
は Result Coercion、 Serialization の両方として利用されます。
Resolver を実装する (図解)
図にまとめると以下のようになります。
GraphQL Scalar で始まる幸せな開発体験
このように Custom Scalar を使うことでサーバーへのリクエストのバリデーションや、出力の際のバリデーション & 値の変換も行うことができるようになります。
ライブラリを導入する
よく使う Custom Scalar をまとめたレポジトリはいくつかあります。
graphql グループ配下の https://github.com/graphql/graphql-scalars や、The Guild がメンテナンスをしている https://github.com/Urigo/graphql-scalars などです。
こうしたライブラリを利用することで簡単に GraphQL Scalar を導入できます。
サンプルで実装してみたのでよければ参照ください。
導入前は
type Tenant {
id: ID!
name: String!
latitude: Float
longitude: Float
page: String
createdAt: String!
updatedAt: String!
}
こんなスキーマだったのが、
type Tenant {
id: ID!
name: String!
latitude: Latitude
longitude: Longitude
page: URL
createdAt: DateTimeISO!
updatedAt: DateTimeISO!
}
こんなに厳密になりました! ハッピー😊
実際に効果を試してみる
実際にサーバーにリクエストを送ってみます。(https://graphql-scalar-sample.deno.dev/graphql で確認できます)
正しい値で Mutation をリクエストするとうまくいきますが
例えば URL 型がついているところに不正な文字を入れるとエラーが返ってくるのがわかります。
まとめ
GraphQL の値のバリデーション、Serialize は極力 Scalar type でやるのが良いです。
その際に Build-in Scalar で要件を満たさない場合は、Custom Scalar を利用することができます。
また、https://github.com/Urigo/graphql-scalars というライブラリには、汎用的な Custom Scalar が定義されていて非常に便利です。
(ちなみに、Custom Scalar を利用することで Codegen の際にも狙った型をつけることができ開発体験が爆上がりしました。その話はまた別の機会にまとめます。)
Discussion