Chapter 05

カスタムスカラ型の導入

さき(H.Saki)
さき(H.Saki)
2023.03.07に更新

この章について

GraphQLクエリで取得するフィールドは全てスカラ型になっている必要があります。

query {
  user(name: "hsaki") {
    id # ID型(スカラ型)
    name # 文字列型(スカラ)
    projectV2(number: 1) {
      title # 文字列型(スカラ)
    }
  }
}

組み込みでは5つのスカラが用意されており、それぞれがGoの中では対応する適切なデータ型に対応づけられるようになっています。

  • Int: 符号あり32bit整数
  • Float: 符号あり倍精度浮動小数点数
  • Boolean: trueまたはfalse
  • String: UTF‐8の文字列
  • ID: 実態としてはStringですが、unique identifierとしての機能を持ったフィールドであればこのID型にするのが望ましいです。

しかし、これ以外にもスカラ型を自作し用意したい場面が存在します。
この章ではそのようなカスタムスカラ型の導入方法を解説します。

カスタムスカラの具体例

今回利用しているGraphQLスキーマの中から、カスタムスカラを使っている場所を紹介します。

具体例その1: DateTime

日時を表すDateTime型が定義されており、RepositoryオブジェクトのcreatedAtフィールドなどで用いられています。

schema.graphqls
scalar DateTime

type Repository implements Node {
	createdAt: DateTime!
}

何の設定も施さないままgqlgenにてコードを自動生成させると、DateTime型に対応するGo構造体フィールドはstring型になってしまいます。

graph/model/models_gen.go
type Repository struct {
	CreatedAt    string    `json:"createdAt"`
}

これをGo側でもtime.Time型にできるととても便利になるでしょう。これがカスタムスカラ型導入の動機になります。

具体例その2: URI

DateTime型以外にも、IssueやPRのURLを表すためのURI型というカスタムスカラ型も定義されています。

schema.graphqls
scalar URI

type Issue implements Node {
  url: URI!
}

こちらも特別な設定なしではGo側でstring型として扱われてしまいます。これもurl.URL型にしたいです。

graph/model/models_gen.go
type Issue struct {
	URL    string    `json:"url"`
}

カスタムスカラの実装

GraphQLスキーマで定義したカスタムスカラ型に対応付けさせるGoの型を変更するには、以下の3つの方法があります。

  1. サードパーティライブラリに用意されたカスタムスカラ対応ロジックを使う
  2. 独自型にMarshalGQL/UnmarshalGQLメソッドを実装する
  3. MarshalXxx/UnmarshalXxx関数を定義する

方法その1 - サードパーティライブラリに用意されたカスタムスカラ対応型を使う

実装方法

github.com/99designs/gqlgenには、よく使われるであろうカスタムスカラ対応ロジックが用意されています。
独自に定義したカスタムスカラをGoのtime.Time型に対応させるロジックも例に漏れません。

今回スキーマ中のDateTime型をGoのtime.Time型にさせたいので、gqlgenの設定ファイルgqlgen.ymlの中に以下のように設定を書き加えます。

gqlgen.yml
models:
  ID:
    model:
      - github.com/99designs/gqlgen/graphql.ID
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32
  Int:
    model:
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32s
+  DateTime:
+    model:
+      - github.com/99designs/gqlgen/graphql.Time

gqlgen.ymlを書き換えた後にgqlgen generateコマンドを実行すると、models_gen.goの中に生成されているモデル型が以下のように変わっていることが確認できるはずです。

graph/model/models_gen.go
type Repository struct {
-	CreatedAt    string                 `json:"createdAt"`
+	CreatedAt    time.Time              `json:"createdAt"`
}

github.com/99designs/gqlgenに用意されたカスタムスカラ対応型

github.com/99designs/gqlgen/graphql.Timeのように、gqlgen.ymlに指定することでカスタムスカラの対応Go型を変えることができる仕組みは他にも存在します。

gqlgen.ymlで指定するモデル 対応づくGoでの型
github.com/99designs/gqlgen/graphql.Any interface{}
github.com/99designs/gqlgen/graphql.Boolean bool
github.com/99designs/gqlgen/graphql.Float float64
github.com/99designs/gqlgen/graphql.ID string
github.com/99designs/gqlgen/graphql.Int int
github.com/99designs/gqlgen/graphql.Int32 int32
github.com/99designs/gqlgen/graphql.Int64 int64
github.com/99designs/gqlgen/graphql.IntID int
github.com/99designs/gqlgen/graphql.Map map[string]interface{}
github.com/99designs/gqlgen/graphql.String string
github.com/99designs/gqlgen/graphql.Time time.Time
github.com/99designs/gqlgen/graphql.Uint uint
github.com/99designs/gqlgen/graphql.Uint32 uint32
github.com/99designs/gqlgen/graphql.Uint64 uint64
github.com/99designs/gqlgen/graphql.Upload graphql.Upload構造体

方法その2 - 独自型にMarshalGQL/UnmarshalGQLメソッドを実装する

github.com/99designs/gqlgenに用意されていない型に自分のカスタムスカラ型を対応させたい場合というのも存在します。
今回の場合URI型がその例です。

schema.graphqls
scalar URI

GraphQLのカスタムスカラ型に対応づけさせたいGoの構造体は、graphql.Marshalerインターフェースとgraphql.Unmarshalerインターフェースを満たす必要があります。

type Marshaler interface {
	MarshalGQL(w io.Writer)
}

type Unmarshaler interface {
	UnmarshalGQL(v interface{}) error
}

例えば今回自分で定義したMyURL型にカスタムスカラ型URIを対応づけさせるためには、まずMyURL型にMarshalGQL/UnmarshalGQLメソッドを実装する必要があります。

graph/model/mymodel.go
type MyURL struct {
	url.URL
}

// MarshalGQL implements the graphql.Marshaler interface
func (u MyURL) MarshalGQL(w io.Writer) {
	io.WriteString(w, fmt.Sprintf(`"%s"`, u.URL.String()))
}

// UnmarshalGQL implements the graphql.Unmarshaler interface
func (u *MyURL) UnmarshalGQL(v interface{}) error {
	switch v := v.(type) {
	case string:
		if result, err := url.Parse(v); err != nil {
			return err
		} else {
			u = &MyURL{*result}
		}
		return nil
	case []byte:
		result := &url.URL{}
		if err := result.UnmarshalBinary(v); err != nil {
			return err
		}
		u = &MyURL{*result}
		return nil
	default:
		return fmt.Errorf("%T is not a url.URL", v)
	}
}

こうして作ったMyURL型を使うようにgqlgen.yml内で設定を記述し、gqlgen generateコマンドでコードを再生成させると、URI型を利用していたIssue.URLフィールドの定義が変わることが確認できます。

gqlgen.yml
models:
+  URI:
+    model:
+      - github.com/saki-engineering/graphql-sample/graph/model.MyURL
graph/model/models_gen.go
type Issue struct {
-	URL          string                   `json:"url"`
+	URL          MyURL                    `json:"url"`
}

方法その3 - 自分でMarshalXxx/UnmarshalXxx関数を定義する

方法その2のときは、カスタムスカラURI型をマッピングするのが自分で定義したMyURL型だったため、自由にMarshalGQL/UnmarshalGQLメソッドを追加することができました。
しかし、例えばカスタムスカラURI型を標準パッケージ内にあるurl.URL型にマッピングさせたいということを考えるならば、方法その2は使えません。
標準パッケージnet/urlに定義されている既存の型url.URLにメソッドを追加実装することができないからです。

このように、既存のサードパッケージ型・標準パッケージ型といった、自分の判断でMarshalGQL/UnmarshalGQLメソッドを追加できないようなパターンが存在します。
そのような場合には、MarshalXxx/UnmarshalXxx関数を用意することになります。

graph/model/mymodel.go
func MarshalURI(u url.URL) graphql.Marshaler {
	return graphql.WriterFunc(func(w io.Writer) {
		io.WriteString(w, fmt.Sprintf(`"%s"`, u.String()))
	})
}

func UnmarshalURI(v interface{}) (url.URL, error) {
	switch v := v.(type) {
	case string:
		u, err := url.Parse(v)
		if err != nil {
			return url.URL{}, err
		}
		return *u, nil
	case []byte:
		u := &url.URL{}
		if err := u.UnmarshalBinary(v); err != nil {
			return url.URL{}, err
		}
		return *u, nil
	default:
		return url.URL{}, fmt.Errorf("%T is not a url.URL", v)
	}
}

このようにMarshalURI/UnmarshalURI関数を定義した後に、gqlgen.yml内に以下のように設定を書き加えコードを再生させれば、見事カスタムスカラURI型を標準パッケージ内にあるurl.URL型に紐づけることができます。

gqlgen.yml
models:
+  URI:
+    model:
-      - github.com/saki-engineering/graphql-sample/graph/model.MyURL
+      - github.com/saki-engineering/graphql-sample/graph/model.URI
graph/model/models_gen.go
type Issue struct {
-	URL          string                   `json:"url"`
+	URL          url.URL                  `json:"url"`
}