120 Using Enums and Custom Scalars in gqlgen
120 Using Enums and Custom Scalars in gqlgen
gqlgen has become the de facto library for building GraphQL APIs in Go. Its main strengths lie in being strongly typed, along with how easy it makes extending the core GraphQL concepts through enums and custom scalars.
In article number 120 of the GraphQL Engineering series, I’ll dig into the use of enums and custom scalars with gqlgen, complete with code examples, query simulations, and process diagrams that make everything easier to understand.
Why Do We Need Enums and Custom Scalars?
By default, GraphQL provides a handful of primitive scalars such as Int, Float, String, Boolean, and ID. In real-world practice, however, the data types we need are often more complex or have a limited range of valid values, for example:
- Enum: Transaction status (
PENDING,SUCCESS,FAILED) - Custom Scalar:
DateTime,BigInt,Email
With enums, we guarantee that only a specific subset of values is accepted. With custom scalars, we can constrain and validate more logic at the schema level.
Implementing Enums in gqlgen
Suppose we want to build a payment API with a transaction status based on an enum. Let’s start by defining the schema:
1# schema.graphql
2enum TransactionStatus {
3 PENDING
4 SUCCESS
5 FAILED
6}
7
8type Transaction {
9 id: ID!
10 amount: Int!
11 status: TransactionStatus!
12}
13
14type Query {
15 transaction(id: ID!): Transaction
16}After updating the schema, run:
1go run github.com/99designs/gqlgen generategqlgen automatically generates the enum as Go code:
1// generated.go, auto-generated snippet
2type TransactionStatus string
3
4const (
5 TransactionStatusPending TransactionStatus = "PENDING"
6 TransactionStatusSuccess TransactionStatus = "SUCCESS"
7 TransactionStatusFailed TransactionStatus = "FAILED"
8)A Simple Resolver Implementation
1func (r *queryResolver) Transaction(ctx context.Context, id string) (*model.Transaction, error) {
2 return &model.Transaction{
3 ID: id,
4 Amount: 150000,
5 Status: model.TransactionStatusSuccess,
6 }, nil
7}Query Simulation
1query {
2 transaction(id: "trx01") {
3 id
4 amount
5 status
6 }
7}Response:
1{
2 "data": {
3 "transaction": {
4 "id": "trx01",
5 "amount": 150000,
6 "status": "SUCCESS"
7 }
8 }
9}Implementing Custom Scalars in gqlgen
Sometimes the default scalars aren’t enough. Take time values, for example. We typically want a DateTime scalar, but GraphQL itself doesn’t provide that type natively.
1. Define the Scalar in the Schema
1scalar DateTime
2
3type UserAction {
4 id: ID!
5 actedAt: DateTime!
6}
7
8type Query {
9 action(id: ID!): UserAction
10}2. Add the Go Implementation
Add an entry to gqlgen.yml:
1models:
2 DateTime:
3 model:
4 - github.com/your/module/path/graph/customscalars.DateTimeThen create the file customscalars/datetime.go:
1package customscalars
2
3import (
4 "fmt"
5 "io"
6 "time"
7)
8
9type DateTime time.Time
10
11const dateFormat = time.RFC3339
12
13func (d *DateTime) UnmarshalGQL(v interface{}) error {
14 str, ok := v.(string)
15 if !ok {
16 return fmt.Errorf("DateTime must be a string")
17 }
18 t, err := time.Parse(dateFormat, str)
19 if err != nil {
20 return err
21 }
22 *d = DateTime(t)
23 return nil
24}
25
26func (d DateTime) MarshalGQL(w io.Writer) {
27 t := time.Time(d)
28 _, _ = io.WriteString(w, fmt.Sprintf("%q", t.Format(dateFormat)))
29}3. Example Usage in a Resolver
1func (r *queryResolver) Action(ctx context.Context, id string) (*model.UserAction, error) {
2 actedAt := customscalars.DateTime(time.Now())
3 return &model.UserAction{
4 ID: id,
5 ActedAt: actedAt,
6 }, nil
7}4. Query Simulation
1query {
2 action(id: "act123") {
3 id
4 actedAt
5 }
6}1{
2 "data": {
3 "action": {
4 "id": "act123",
5 "actedAt": "2024-06-13T21:17:00Z"
6 }
7 }
8}Enum vs. Custom Scalar Comparison
| Feature | Enum | Custom Scalar |
|---|---|---|
| Validation | Fixed value matching | Structure/type value validation |
| Popular use cases | Status, role, phase | Date, Email, JSON, BigInt |
| Effect in GraphQL | Strongly typed | Flexible/loose, custom parsing |
| Automatic in Go? | Yes | Requires extra code |
Custom Scalar Serialization Flow Diagram
To clarify how a custom scalar is processed, here is the flow via mermaid:
flowchart TD
ClientRequest["Client sends a query with a custom scalar value"]
Server("GraphQL Server (gqlgen)")
Unmarshal("UnmarshalGQL() Custom Scalar")
Resolver("Query Resolver")
Marshal("MarshalGQL() Custom Scalar")
Response["Server sends the response back to the client"]
ClientRequest --> Server
Server --> Unmarshal
Unmarshal --> Resolver
Resolver --> Marshal
Marshal --> Response
Best Practices
- Always Test Your Custom Scalars
- Cover parsing edge cases (string, null, invalid format, etc.)
- Keep Enums Constrained
- Use enums only for a limited and fixed set of values (not dynamic ones)
- Use Test Tables
- Coverage is easy to extend, and scalars are easy to guard against regressions
Conclusion
Using enums and custom scalars in gqlgen with Go makes your API more robust, your data types safer, and your validation logic easier to maintain. Custom scalars do require a bit of boilerplate, but the payoff is tremendous, especially for maintainability and debuggability.
Don’t hesitate to add new scalars or enums as your schema grows more complex. Just keep the implementation clean and well tested.
References:
Happy experimenting with gqlgen, enums, and custom scalars! If you have questions or interesting experiences to share, drop them in the comments.