Skip to content

Gin framework oneof validation tag: pitfalls and best practices of single-quote notation ​

Diagram of installing the Kubernetes Dashboard control panel

Problem site ​

After taking over a Gin framework project, testing feedback indicated that there was a problem with the query parameters of the list interface—the newly added notification type could not be correctly recognized. I looked at the backend API interface code:

go
type NotificationListRequest struct {
    core.PaginationBaseRequest
    Type string `json:"type" form:"type" binding:"oneof='email''sms''whatsapp''slack'''"`
}

The moment I saw this line of code, my coding OCD kicked in—why oneof Should each value be wrapped in single quotes inside?

In my understanding, Gin's oneof The verification label should look like this:

go
// Official example style, clearly list all types.
Type string `binding:"oneof=email sms whatsapp slack dingding"`

Spaces are concise, clear, and obvious at a glance. Yet this style wrapped in single quotes before us is baffling.

Project preparation ​

To illustrate this issue, I created a demo project Through two contrasting implementations, demonstrate the differences brought about by different writing styles. You can also directly view or download the source code and follow the steps below to verify.

Project Structure ​

├── README.md
├── cmd
│   ├── gin-binding-grace      # Elegant implementation
│   │   └── main.go
│   └── gin-binding-oops       # A problematic implementation
│       └── main.go
├── go.mod
├── go.sum
└── internal
    └── pkg
        └── core
            └── response.go    # Generic response structure

Generic response structure ​

internal/pkg/core/response.go Defined a unified API response format and pagination parameters:

go
package core

// PaginationBaseRequest defines common pagination request parameters.
type PaginationBaseRequest struct {
    Number int `json:"number" form:"number,default=1"`
    Size   int `json:"size" form:"size,default=10"`
}

// PaginationResponse wraps paginated data
type PaginationResponse struct {
    PaginationBaseRequest
    Data any `json:"data"`
}

// Response Unified API response format
type Response struct {
    Message string `json:"message"`
    Code    int    `json:"code"`
    Data    any    `json:"data,omitempty"`
}

// Business status code constants
const (
    CodeWithSuccess = 100100
    CodeWithFailed  = 100101
)

// Success successful response
func Success(data interface{}) *Response {
    return &Response{
        Code:    CodeWithSuccess,
        Message: "success",
        Data:    data,
    }
}

// Error error response
func Error(message string) *Response {
    return &Response{
        Code:    CodeWithFailed,
        Message: message,
    }
}

Bad API design (gin-binding-oops) ​

Specific implementation ​

go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/fishfinal/go-binding-demo/internal/pkg/core"
	"github.com/gin-gonic/gin"
)

type NotificationListRequest struct {
	core.PaginationBaseRequest
	// Note this line: it is wrapped in single quotes and has no space separator.
	Type string `json:"type" form:"type" binding:"oneof='email''sms''whatsapp''slack'''"`
}

func main() {
	r := gin.Default()
	v1 := r.Group("/api/v1")
	{
		v1.GET("/notifications", func(ctx *gin.Context) {
			var parameters *NotificationListRequest
			if err := ctx.ShouldBindQuery(&parameters); err != nil {
				ctx.JSON(http.StatusBadRequest, core.Error(err.Error()))
				return
			}

			marshal, _ := json.MarshalIndent(parameters, "", "  ")
			fmt.Println(string(marshal))

			pagination := core.PaginationResponse{
				PaginationBaseRequest: parameters.PaginationBaseRequest,
				Data:                  []any{},
			}
			ctx.JSON(http.StatusOK, core.Success(pagination))
			return
		})
	}
	log.Fatal(r.Run(":8001"))
}

Test results ​

bash
# Test various request scenarios
➜ curl "http://127.0.0.1:8001/api/v1/notifications?type="
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

➜ curl "http://127.0.0.1:8001/api/v1/notifications?type"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

➜ curl "http://127.0.0.1:8001/api/v1/notifications"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

➜ curl "http://127.0.0.1:8001/api/v1/notifications?type=whatsapp"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

Server log output:

json
{
  "number": 1,
  "size": 10,
  "type": ""
}
[GIN] 2026/07/08 - 00:02:11 | 200 | 65.478µs | 127.0.0.1 | GET "/api/v1/notifications?type="

{
  "number": 1,
  "size": 10,
  "type": ""
}
[GIN] 2026/07/08 - 00:02:24 | 200 | 59.166µs | 127.0.0.1 | GET "/api/v1/notifications?type="

{
  "number": 1,
  "size": 10,
  "type": ""
}
[GIN] 2026/07/08 - 00:02:27 | 200 | 68.184µs | 127.0.0.1 | GET "/api/v1/notifications?type"

{
  "number": 1,
  "size": 10,
  "type": ""
}
[GIN] 2026/07/08 - 00:02:29 | 200 | 71.177µs | 127.0.0.1 | GET "/api/v1/notifications"

{
  "number": 1,
  "size": 10,
  "type": "whatsapp"
}
[GIN] 2026/07/08 - 00:02:49 | 200 | 77.479µs | 127.0.0.1 | GET "/api/v1/notifications?type=whatsapp"

Problem Analysis ​

All requests returned success, including cases where invalid values were passed in. Although oneof='email''sms''whatsapp''slack''' At runtime, it can be correctly parsed into 5 valid values, but the following issues exist:

  • Extremely poor readability: At first glance, it is completely impossible to identify which valid values there are.

  • easily misunderstood: Other developers might think this is a complete string.

  • Difficult to maintain: When adding a new type, I don’t know where it should be added or how to add it.

  • Unclear null value handling:type When empty,Type It is bound to an empty string, but the business semantics of "query all" are unclear.

The Truth About Single Quote Notation ​

The developer's original intent ​

write oneof='email''sms''whatsapp''slack''' At the time, the developer’s original intention was to express Type The field can support 5 valid values:

  • email

  • sms

  • whatsapp

  • slack

  • empty string ''

Actual parsing behavior ​

In Go struct tags, adjacent string literals are automatically concatenated. So:

'email''sms''whatsapp''slack'''
    ↓ Adjacent string concatenation
'email' + '' + 'sms' + '' + 'whatsapp' + '' + 'slack' + ''

Here's '' represents an empty string, so actually this tag Indeed represents 5 values, and can be correctly parsed at runtime.

So where is the problem? ​

The problem is not runtime parsing, but the readability and maintainability of the code!

This way of writing makes the code difficult to understand. When Developer A sees this code, it may take several minutes to understand that it represents 5 values. If it were written in the standard format, one could see at a glance which valid values there are.

Elegant API design (gin-binding-grace) ​

Specific implementation ​

go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/fishfinal/go-binding-demo/internal/pkg/core"
	"github.com/gin-gonic/gin"
)

type NotificationListRequest struct {
	core.PaginationBaseRequest
	// Clear and explicit: use spaces as separators, set default values, and all types are clear at a glance.
	Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack"`
}

func main() {
	r := gin.Default()
	v1 := r.Group("/api/v1")
	{
		v1.GET("/notifications", func(ctx *gin.Context) {
			var parameters *NotificationListRequest
			if err := ctx.ShouldBindQuery(&parameters); err != nil {
				ctx.JSON(http.StatusBadRequest, core.Error(err.Error()))
				return
			}

			marshal, _ := json.MarshalIndent(parameters, "", "  ")
			fmt.Println(string(marshal))

			pagination := core.PaginationResponse{
				PaginationBaseRequest: parameters.PaginationBaseRequest,
				Data:                  []any{},
			}
			ctx.JSON(http.StatusOK, core.Success(pagination))
			return
		})
	}
	log.Fatal(r.Run(":8002"))
}

Test results ​

bash
➜ curl "http://127.0.0.1:8002/api/v1/notifications?type=whatsapp"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

➜ curl "http://127.0.0.1:8002/api/v1/notifications?type="
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

➜ curl "http://127.0.0.1:8002/api/v1/notifications?type"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

➜ curl "http://127.0.0.1:8002/api/v1/notifications"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}

Server-side log output:

json
{
  "number": 1,
  "size": 10,
  "type": "whatsapp"
}
[GIN] 2026/07/08 - 00:07:48 | 200 | 357.674µs | 127.0.0.1 | GET "/api/v1/notifications?type=whatsapp"

{
  "number": 1,
  "size": 10,
  "type": "all"
}
[GIN] 2026/07/08 - 00:07:57 | 200 | 152.8µs | 127.0.0.1 | GET "/api/v1/notifications?type="

{
  "number": 1,
  "size": 10,
  "type": "all"
}
[GIN] 2026/07/08 - 00:08:14 | 200 | 74.697µs | 127.0.0.1 | GET "/api/v1/notifications?type"

{
  "number": 1,
  "size": 10,
  "type": "all"
}
[GIN] 2026/07/08 - 00:08:20 | 200 | 78.425µs | 127.0.0.1 | GET "/api/v1/notifications"

Core advantages of elegant implementation ​

  • Clear enumeration values:oneof=all email sms whatsapp slack Clear at a glance

  • Reasonable default values:default=all Give null values a clear business meaning

  • Easy to extend: Add dingding Just add it at the end.

  • Self-documenting: The code itself is documentation

Comparison when adding the dingding type ​

Bad practice (continuing to pile onto the chaos) ​

go
// ❌ Continuing to concatenate, it becomes increasingly difficult to understand
Type string `binding:"oneof='email''sms''whatsapp''slack''dingding'''"`

// ❌ Added arbitrarily; not sure if it's correct
Type string `binding:"oneof='email''sms''whatsapp''slack'''dingding'"`

An elegant approach (clear extension) ​

go
// ✅ Clearly list all types at a glance
Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`

Comparison of oneof tag syntax ​

writing styleAnalysis resultreadabilityRecommendation rating
oneof='email''sms''whatsapp''slack'''["email", "sms", "whatsapp", "slack", ""]❌ Extremely poorNot recommended
oneof='email' 'sms' 'whatsapp' 'slack' ''["'email'", "'sms'", "'whatsapp'", "'slack'", "''"]⚠️ GeneralNot recommended
oneof=email sms whatsapp slack["email", "sms", "whatsapp", "slack"]✅ ClearRecommend
oneof=all email sms whatsapp slack dingding["all", "email", "sms", "whatsapp", "slack", "dingding"]✅ ClearStrongly recommend

Best practices ​

1. Use standard notation ​

go
// ✅ Recommended: no quotes, space-separated
`binding:"oneof=email sms whatsapp slack dingding"`

// ✅ Recommended: Handle null values with default values
`form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`

2. Use constants to manage enumeration values ​

go
const (
    TypeAll      = "all"
    TypeEmail    = "email"
    TypeSMS      = "sms"
    TypeWhatsApp = "whatsapp"
    TypeSlack    = "slack"
    TypeDingDing = "dingding"
)

type NotificationListRequest struct {
    core.PaginationBaseRequest
    Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`
}

3. Add code comments ​

go
type NotificationListRequest struct {
    core.PaginationBaseRequest

    // Type notification type filtering
    // Supported: all, email, sms, whatsapp, slack, dingding
    // Default: all (query all types)
    Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`
}

Summary ​

Back to the original question:oneof='email''sms''whatsapp''slack''' This tag can indeed be correctly parsed at runtime into 5 valid values (email, sms, whatsapp, slack, empty string).

but Code is not only for machines to execute, but also for humans to read.. By comparing the two implementations, we can clearly see:

  • Readability is an important indicator of code quality.: Good code should let readers understand its intent at a glance.

  • Standards can prevent low-level mistakes. Unified writing standards can reduce comprehension and maintenance costs.

  • Details determine success or failure.: A difference in how a tag is written affects the collaboration efficiency of the entire team.

  • Good design is evolved.: from gin-binding-oops to gin-binding-grace The shift reflects a pursuit of code quality.

Therefore, we should choose a clearer and more standardized way of writing:

go
// Clear and concise, easy to understand at a glance.
binding:"oneof=all email sms whatsapp slack dingding"

Every detail of the code deserves deep thought. Good code must not only run correctly, but also let readers quickly understand its intent. This is true "elegance."

Complete example code includes gin-binding-oops and gin-binding-grace Two comparative implementations, feel free to refer to and learn from.

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]