You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Gin框架中实现同时支持驼峰与蛇形命名的输入验证?

问题描述

我正在迁移一个Python编写的API,原API通过Pydantic支持同时接受camelCase和snake_case格式的请求参数,示例如下:

允许的请求格式1(camelCase):

{
    "someInput": "nice"
}

允许的请求格式2(snake_case):

{
    "some_input": "nice"
}

Python实现代码:

from pydantic import BaseModel

def to_camel(string):
    words = string.split('_')
    return words[0] + ''.join(word.capitalize() for word in words[1:])

class InputModel(BaseModel):
    some_input: str

    class Config:
        alias_generator = to_camel
        allow_population_by_field_name = True

现在我在Go的Gin框架中实现等效功能,当前路由和处理函数代码:

func Routes(router *gin.Engine) {
    v1 := router.Group("/v1")
    {
        v1.POST("/shipments", controllers.ShipmentCreator)
    }
}

func ShipmentCreator(ctx *gin.Context) {
    ResponseController := new(internal.OutputModel)
    var body domain.ShipmentsInputModel
    if err := ctx.BindJSON(&body); err != nil {
        fmt.Println(err)
    }
    validate := validator.New()
    err := validate.Struct(body)
    if err != nil {
        var validationErrors validator.ValidationErrors
        errors.As(err, &validationErrors)
        for _, validationError := range validationErrors {
            ResponseController.AddError(internal.ErrorsModel{
                Parameter: validationError.Field(),
                Message:   validationError.Error(),
            })
        }
        ctx.JSON(http.StatusBadRequest, ResponseController)
        return
    }
    // 后续业务逻辑
}

输入结构体定义:

type ShipmentsInputModel struct {
    LotId   string   `json:"lotId" tag:"lot_id" alias:"lot_id" validate:"required"`
}

当发送snake_case格式请求时:

{
   "lot_id": "someLotId"
}

系统返回错误:

"message": "Key: 'ShipmentsInputModel.LotId' Error:Field validation for 'LotId' failed on the 'required' tag"

需要让Gin框架同时接受camelCase和snake_case格式的输入。


解决方案

Gin默认依赖标准库encoding/json,不支持单个字段绑定多个JSON键,以下是三种可行的实现方式:

方法1:自定义UnmarshalJSON方法(快速实现)

为输入结构体手动实现UnmarshalJSON方法,同时处理两种格式的字段映射:

type ShipmentsInputModel struct {
    LotId string `validate:"required"`
}

func (s *ShipmentsInputModel) UnmarshalJSON(data []byte) error {
    // 定义临时结构体,包含两种格式的字段
    type temp struct {
        LotIDCamel string `json:"lotId"`
        LotIDSnake string `json:"lot_id"`
    }
    var t temp
    if err := json.Unmarshal(data, &t); err != nil {
        return err
    }
    // 优先取camelCase值,为空则用snake_case值
    if t.LotIDCamel != "" {
        s.LotId = t.LotIDCamel
    } else {
        s.LotId = t.LotIDSnake
    }
    return nil
}

该方式逻辑简单,适合单个结构体快速适配两种输入格式,后续validator校验可正常执行。

方法2:自定义Gin绑定器(复用性强)

如果需要多个结构体复用双格式解析逻辑,可以自定义Gin绑定器,通过反射实现通用字段映射:

import (
    "bytes"
    "fmt"
    "io"
    "reflect"
    "strings"

    "github.com/gin-gonic/gin"
)

// MultiFormatBinding 支持camelCase和snake_case输入的自定义绑定器
type MultiFormatBinding struct{}

func (m MultiFormatBinding) Bind(ctx *gin.Context, obj interface{}) error {
    // 先尝试默认camelCase解析
    if err := ctx.ShouldBindJSON(obj); err == nil {
        return nil
    }
    // 读取请求体并重置,避免后续读取失败
    body, err := io.ReadAll(ctx.Request.Body)
    if err != nil {
        return err
    }
    ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))

    // 解析为原始map,处理键名映射
    var raw map[string]interface{}
    if err := json.Unmarshal(body, &raw); err != nil {
        return err
    }

    val := reflect.ValueOf(obj).Elem()
    for k, v := range raw {
        // 尝试snake_case转camelCase后匹配字段
        camelKey := toCamel(k)
        field := val.FieldByName(strings.Title(camelKey))
        if field.IsValid() && field.CanSet() {
            if err := setFieldValue(field, v); err != nil {
                return err
            }
            continue
        }
        // 直接匹配原始键名(首字母大写)
        field = val.FieldByName(strings.Title(k))
        if field.IsValid() && field.CanSet() {
            if err := setFieldValue(field, v); err != nil {
                return err
            }
        }
    }
    return nil
}

// toCamel 将snake_case转为camelCase
func toCamel(s string) string {
    parts := strings.Split(s, "_")
    for i := 1; i < len(parts); i++ {
        parts[i] = strings.Title(parts[i])
    }
    return strings.Join(parts, "")
}

// setFieldValue 处理不同类型的字段赋值
func setFieldValue(field reflect.Value, value interface{}) error {
    val := reflect.ValueOf(value)
    if val.Type().AssignableTo(field.Type()) {
        field.Set(val)
        return nil
    }

    // 处理常见类型转换(JSON数字默认是float64)
    switch field.Type().Kind() {
    case reflect.String:
        field.SetString(fmt.Sprintf("%v", value))
    case reflect.Int, reflect.Int64:
        floatVal, ok := value.(float64)
        if !ok {
            return fmt.Errorf("cannot convert %v to int", value)
        }
        field.SetInt(int64(floatVal))
    case reflect.Float64:
        floatVal, ok := value.(float64)
        if ok {
            field.SetFloat(floatVal)
        } else {
            return fmt.Errorf("cannot convert %v to float64", value)
        }
    default:
        return fmt.Errorf("unsupported field type: %s", field.Type().Kind())
    }
    return nil
}

在处理函数中使用自定义绑定器:

func ShipmentCreator(ctx *gin.Context) {
    ResponseController := new(internal.OutputModel)
    var body domain.ShipmentsInputModel
    if err := ctx.MustBindWith(&body, MultiFormatBinding{}); err != nil {
        fmt.Println(err)
    }
    // 后续校验逻辑保持不变
    validate := validator.New()
    err := validate.Struct(body)
    if err != nil {
        // 错误处理逻辑
    }
}

方法3:使用支持多别名的JSON库(配置简单)

替换Gin默认的JSON解析器为支持多别名的第三方库json-iterator/go:

  1. 安装依赖:
go get github.com/json-iterator/go
  1. 配置Gin使用该库,并在结构体标签中指定多个别名:
import (
    "github.com/gin-gonic/gin"
    "github.com/json-iterator/go"
)

// 初始化支持多别名的JSON解析器
var json = jsoniter.Config{
    TagKey:             "json",
    AllowDuplicateKeys: true, // 允许重复键(实际取最后一个有效值)
}.Froze()

// 替换Gin默认的JSON处理逻辑
func init() {
    gin.DefaultJSONMarshaler = &gin.JSONMarshaler{
        JSONSerializer: json,
    }
}

// 结构体定义:用逗号分隔多个别名
type ShipmentsInputModel struct {
    LotId string `json:"lotId,lot_id" validate:"required"`
}

该方式配置简单,无需编写额外解析逻辑,适合大规模复用场景。


内容的提问来源于stack exchange,提问作者Ivan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 09:43:15