Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
28 Oct 2025 · 5 min read ·Article 120 / 125
Go

120 Using Enums and Custom Scalars in gqlgen

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

graphql
 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:

bash
1go run github.com/99designs/gqlgen generate

gqlgen automatically generates the enum as Go code:

go
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

go
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

graphql
1query {
2  transaction(id: "trx01") {
3    id
4    amount
5    status
6  }
7}

Response:

json
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

graphql
 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:

yaml
1models:
2  DateTime:
3    model:
4      - github.com/your/module/path/graph/customscalars.DateTime

Then create the file customscalars/datetime.go:

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

go
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

graphql
1query {
2  action(id: "act123") {
3    id
4    actedAt
5  }
6}
Response:
json
1{
2  "data": {
3    "action": {
4      "id": "act123",
5      "actedAt": "2024-06-13T21:17:00Z"
6    }
7  }
8}


Enum vs. Custom Scalar Comparison

FeatureEnumCustom Scalar
ValidationFixed value matchingStructure/type value validation
Popular use casesStatus, role, phaseDate, Email, JSON, BigInt
Effect in GraphQLStrongly typedFlexible/loose, custom parsing
Automatic in Go?YesRequires extra code

Custom Scalar Serialization Flow Diagram

To clarify how a custom scalar is processed, here is the flow via mermaid:

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

  1. Always Test Your Custom Scalars
    • Cover parsing edge cases (string, null, invalid format, etc.)
  2. Keep Enums Constrained
    • Use enums only for a limited and fixed set of values (not dynamic ones)
  3. 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.

Related Articles

💬 Comments