目录

Go 自定义 json 解析规则

在使用 go json 序列化数据时,有些类型/结构序列化没办法满足业务需求,比如,日期格式是“2024/05/20 14:30:00”,Go默认的time.Time根本解析不了,解析出来的结果没办法满足业务需求,今天分享如何使用自定义 JSON 解析规则完美解决,希望对大家有所帮助。

为什么需要自定义 JSON 解析?

Go 的 encoding/json 包提供了默认的序列化和反序列化能力,但实际开发中,默认规则往往满足不了复杂场景。

比如这几种常见情况:

  • 第三方接口返回的字段名是下划线命名(如user_name),但我们代码用驼峰命名(UserName);
  • 日期格式不标准(如“2024-05-20”“2024/05/20”),默认 time.Time 解析会报错;
  • 枚举值需要转换(如数字 1 转成“待支付”字符串);
  • 嵌套结构体需要扁平化处理,或某些字段需要特殊计算后再序列化。

这些场景下,就必须通过自定义解析规则来突破默认限制。

核心思路有两种:

  • 一是通过结构体 tag 做简单配置,
  • 二是实现 json.Marshaler 和 json.Unmarshaler 接口做复杂定制。

基础:结构体 Tag 配置

如果只是字段名映射、忽略空值、指定默认值等简单需求,用结构体 tag 是最高效的方式,不用写一行额外代码。

常用的 tag 关键字有4个:json、omitempty、string、default。

1. 核心 Tag:字段名映射与基础配置

最常用的场景是字段名映射,比如把 Go 的驼峰字段对应到 JSON 的下划线字段,同时忽略空值字段。

直接看代码示例:

package main

import (
    "encoding/json"
    "fmt"
)

// 订单结构体,通过tag配置JSON解析规则
type Order struct {
    // json:"order_id":指定JSON字段名为order_id
    OrderID string `json:"order_id"`
    // omitempty:字段为空时不序列化到JSON中
    UserName string `json:"user_name,omitempty"`
    // 不指定json tag:默认用字段名作为JSON键(UserName -> UserName)
    Amount float64
    // -:序列化时忽略该字段,无论值是什么
    Secret string `json:"-"`
}

func main() {
    // 构造测试数据
    order := Order{
        OrderID:  "ORD20240520001",
        UserName: "", // 空值,会被omitempty忽略
        Amount:   99.9,
        Secret:   "abc123", // 会被忽略
    }

    // 序列化:将结构体转JSON
    data, err := json.MarshalIndent(order, "", "  ")
    if err != nil {
        fmt.Printf("序列化失败:%v\n", err)
        return
    }
    fmt.Println("序列化结果:")
    fmt.Println(string(data))
}

输出结果分析:UserName 因空值被忽略,Secret 被强制忽略,最终 JSON 只保留 order_id 和 Amount 两个字段,字段名也按 tag 指定的映射正确转换,完全符合对接第三方接口的常见需求。

2. 实用 Tag:string 与 default

string tag 能强制将数值类型序列化为字符串(避免前端解析数字精度丢失),default tag 可指定反序列化时的默认值(需结合第三方库,如 github.com/go-playground/validator)。

示例:

import "github.com/go-playground/validator"

// 产品结构体
type Product struct {
    // string:序列化时将int64转为字符串
    ID    int64  `json:"id,string"`
    // default:反序列化时若字段为空,默认值为"未分类"
    Category string `json:"category" validate:"required" default:"未分类"`
    Name  string `json:"name"`
}

func main() {
    // 1. 测试string tag序列化
    product := Product{ID: 1234567890123456789, Name: "无线耳机", Category: "数码"}
    data, _ := json.MarshalIndent(product, "", "  ")
    fmt.Println("带string tag的序列化结果:")
    fmt.Println(string(data)) // 输出中id为字符串类型:"id":"1234567890123456789"

    // 2. 测试default tag反序列化
    jsonStr := `{"id":"9876543210987654321", "name":"键盘"}` // 缺少category字段
    var p Product
    json.Unmarshal([]byte(jsonStr), &p)
    // 用第三方库触发默认值赋值
    validate := validator.New()
    validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
        return fld.Tag.Get("json")
    })
    validate.Struct(&p)
    fmt.Println("反序列化后默认值:", p.Category) // 输出"未分类"
}

注意事项:default tag 不是 encoding/json 包的原生功能,需要结合 validator 等第三方库实现,适合反序列化时补全默认字段的场景

进阶:实现 Marshaler/Unmarshaler 接口

当tag满足不了需求时,比如自定义日期格式、枚举值转换、嵌套结构处理,就需要给结构体实现json.Marshaler(序列化)和json.Unmarshaler(反序列化)接口。

这是自定义JSON解析的核心能力,也是实际开发中解决复杂问题的关键。

1. 实战场景1:自定义日期格式解析

这是最常见的复杂场景——第三方接口返回的日期是“2024/05/20 14:30:00”或“2024-05-20”,而Go默认只支持RFC3339格式(如“2024-05-20T14:30:00Z”)。

通过实现接口可完美解决:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

// 自定义日期类型,基于time.Time
type CustomTime time.Time

// 定义常用日期格式
const (
    TimeFormat1 = "2006/01/02 15:04:05"
    TimeFormat2 = "2006-01-02"
)

// 实现json.Marshaler接口:自定义序列化逻辑
func (ct CustomTime) MarshalJSON() ([]byte, error) {
    // 将CustomTime转为time.Time,再格式化为指定字符串
    t := time.Time(ct)
    return json.Marshal(t.Format(TimeFormat1))
}

// 实现json.Unmarshaler接口:自定义反序列化逻辑
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
    // 先去除JSON字符串的引号
    var timeStr string
    if err := json.Unmarshal(data, &timeStr); err != nil {
        return fmt.Errorf("日期字符串解析失败:%w", err)
    }

    // 尝试多种格式解析,适配不同场景
    t, err := time.Parse(TimeFormat1, timeStr)
    if err != nil {
        t, err = time.Parse(TimeFormat2, timeStr)
        if err != nil {
            return fmt.Errorf("不支持的日期格式:%s,仅支持%s和%s", timeStr, TimeFormat1, TimeFormat2)
        }
    }

    // 将解析后的time.Time赋值给CustomTime
    *ct = CustomTime(t)
    return nil
}

// 订单结构体,使用自定义日期类型
type Order struct {
    OrderID   string      `json:"order_id"`
    CreateAt  CustomTime  `json:"create_at"` // 自定义日期字段
    PayTime   *CustomTime `json:"pay_time,omitempty"` // 可选日期字段
}

func main() {
    // 1. 测试序列化:将CustomTime转为指定格式字符串
    now := CustomTime(time.Now())
    order1 := Order{OrderID: "ORD001", CreateAt: now}
    data1, _ := json.MarshalIndent(order1, "", "  ")
    fmt.Println("日期序列化结果:")
    fmt.Println(string(data1))

    // 2. 测试反序列化:解析不同格式的日期字符串
    jsonStr := `{"order_id":"ORD002", "create_at":"2024/05/20 10:00:00", "pay_time":"2024-05-20"}`
    var order2 Order
    if err := json.Unmarshal([]byte(jsonStr), &order2); err != nil {
        fmt.Printf("反序列化失败:%v\n", err)
        return
    }
    fmt.Println("反序列化后创建时间:", time.Time(order2.CreateAt).Format(TimeFormat1))
    fmt.Println("反序列化后支付时间:", time.Time(*order2.PayTime).Format(TimeFormat1))
}

核心逻辑:通过自定义日期类型包装 time.Time,实现接口后接管序列化和反序列化过程,既支持多种格式解析,又能统一输出格式,完全适配第三方接口的不规则日期。

2. 实战场景2:枚举值转换

另一个高频场景是枚举值转换,比如将代码中的字符串枚举(“pending”“paid”)与接口返回的数字(1、2)互转。

示例:

package main

import (
    "encoding/json"
    "fmt"
)

// 订单状态枚举类型
type OrderStatus string

// 定义枚举值
const (
    OrderStatusPending OrderStatus = "pending" // 待支付
    OrderStatusPaid    OrderStatus = "paid"    // 已支付
    OrderStatusCancel  OrderStatus = "cancel"  // 已取消
)

// 枚举值与数字的映射表
var statusToInt = map[OrderStatus]int{
    OrderStatusPending: 1,
    OrderStatusPaid:    2,
    OrderStatusCancel:  3,
}

var intToStatus = map[int]OrderStatus{
    1: OrderStatusPending,
    2: OrderStatusPaid,
    3: OrderStatusCancel,
}

// 实现序列化接口:将枚举字符串转为数字
func (s OrderStatus) MarshalJSON() ([]byte, error) {
    // 查找映射的数字,若不存在返回错误
    if code, ok := statusToInt[s]; ok {
        return json.Marshal(code)
    }
    return nil, fmt.Errorf("不支持的订单状态:%s", s)
}

// 实现反序列化接口:将数字转为枚举字符串
func (s *OrderStatus) UnmarshalJSON(data []byte) error {
    var code int
    if err := json.Unmarshal(data, &code); err != nil {
        return fmt.Errorf("状态码解析失败:%w", err)
    }

    // 查找映射的枚举值
    if status, ok := intToStatus[code]; ok {
        *s = status
        return nil
    }
    return fmt.Errorf("不支持的状态码:%d", code)
}

type Order struct {
    OrderID string       `json:"order_id"`
    Status  OrderStatus  `json:"status"` // 枚举字段
}

func main() {
    // 1. 序列化:将"paid"转为2
    order1 := Order{OrderID: "ORD003", Status: OrderStatusPaid}
    data1, _ := json.MarshalIndent(order1, "", "  ")
    fmt.Println("枚举序列化结果:")
    fmt.Println(string(data1)) // 输出{"order_id":"ORD003","status":2}

    // 2. 反序列化:将1转为"pending"
    jsonStr := `{"order_id":"ORD004", "status":1}`
    var order2 Order
    if err := json.Unmarshal([]byte(jsonStr), &order2); err != nil {
        fmt.Printf("反序列化失败:%v\n", err)
        return
    }
    fmt.Println("反序列化后状态:", order2.Status) // 输出pending
}

高级:使用第三方库简化开发

如果项目中有大量复杂的 JSON 解析需求,重复实现接口会很繁琐,这时可以用第三方库提高效率。

推荐的有两个:easyjson 和 go-json,前者通过代码生成优化性能,后者支持更多灵活配置。

1. easyjson:性能优先,代码生成

easyjson 通过预生成代码替代反射,序列化和反序列化性能比原生 encoding/json 快 3-5 倍,同时支持自定义解析规则。

核心步骤:

// 1. 安装easyjson工具
go get -u github.com/mailru/easyjson/...

// 2. 在结构体所在文件添加注释,指定生成规则
//easyjson:json
type Order struct {
    OrderID string    `json:"order_id"`
    CreateAt time.Time `json:"create_at"`
}

// 3. 生成解析代码
easyjson -all order.go

生成后会得到 order_easyjson.go 文件,里面包含了自动生成的 MarshalJSON 和 UnmarshalJSON 方法,直接调用即可。

如果需要自定义规则,可在生成的代码基础上修改。

2. go-json:灵活配置,原生兼容

go-json 是 encoding/json 的兼容替代库,支持更多 tag 配置和自定义解析器,无需代码生成。

示例:

import (
    "fmt"
    "time"

    "github.com/goccy/go-json"
)

type Order struct {
    OrderID  string    `json:"order_id"`
    CreateAt time.Time `json:"create_at" json:"format:'2006/01/02 15:04:05'"`
}

func main() {
    jsonStr := `{"order_id":"ORD005", "create_at":"2024/05/20 16:30:00"}`
    var order Order
    // 直接使用go-json解析,自动适配自定义日期格式
    if err := json.Unmarshal([]byte(jsonStr), &order); err != nil {
        fmt.Printf("解析失败:%v\n", err)
        return
    }
    fmt.Println("go-json解析结果:", order.CreateAt)
}

常见问题

Q1. 反序列化时日期格式不匹配导致报错

现象:JSON 中的日期是“2024-05-20”,用默认 time.Time 解析时提示“parsing time “2024-05-20” as “2006-01-02T15:04:05Z07:00”: cannot parse "" as “T””。

原因:Go 原生 time.Time 只支持 RFC3339 标准格式,非标准格式需要自定义解析。

解决方案:按照前面“自定义日期格式解析”的方法,包装 time.Time 为自定义类型并实现接口,支持多种格式解析。

Q2. 枚举值转换时出现“不支持的状态码”错误

现象:第三方接口返回状态码4,但我们的映射表中没有对应枚举值,反序列化报错。

原因:映射表未覆盖所有可能的状态码,属于边界场景考虑不足。

解决方案:在反序列化时增加默认值处理,未知状态码映射为“未知状态”,避免程序崩溃

func (s *OrderStatus) UnmarshalJSON(data []byte) error {
    var code int
    if err := json.Unmarshal(data, &code); err != nil {
        return err
    }

    if status, ok := intToStatus[code]; ok {
        *s = status
    } else {
        *s = "unknown" // 未知状态默认值
    }
    return nil
}

Q3. 嵌套结构体解析后部分字段为空

现象:JSON 中有嵌套结构“user":{“name”:“张三”,“age”:28},对应的 Go 结构体中 User 字段解析后为空。

原因:要么是结构体字段名与JSON键不匹配(未用 tag 配置),要么是嵌套结构体的字段未导出(首字母小写)。

解决方案:① 给嵌套结构体字段加正确的 json tag;② 确保嵌套结构体的所有字段首字母大写(导出),

示例:

// 正确的嵌套结构体定义
type Order struct {
    OrderID string `json:"order_id"`
    // 字段名首字母大写,tag对应JSON键
    User User `json:"user"`
}

// 嵌套结构体字段首字母大写
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

Q4. 序列化时 float64 类型出现精度丢失

现象:Go 中的 float64(0.1) 序列化后变为 0.10000000000000001。

原因:float64 类型的固有精度问题,JSON 序列化时会暴露该问题。

解决方案:使用 decimal 库(如github.com/shopspring/decimal)处理小数,自定义序列化和反序列化:

import "github.com/shopspring/decimal"

type Decimal decimal.Decimal

func (d Decimal) MarshalJSON() ([]byte, error) {
    return json.Marshal(decimal.Decimal(d).String())
}

func (d *Decimal) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    dec, err := decimal.NewFromString(s)
    if err != nil {
        return err
    }
    *d = Decimal(dec)
    return nil
}

总结

自定义 JSON 解析没有“万能方案”,关键是根据场景选对方法:

  • 简单字段映射/忽略空值:直接用结构体 tag(json、omitempty),最快最省事;
  • 日期/枚举/小数等特殊类型:包装基础类型并实现 Marshaler/Unmarshaler 接口,灵活可控;
  • 高并发/高性能场景:用 easyjson 生成代码,替代原生反射提升性能;
  • 复杂配置+原生兼容:用 go-json 库,支持更多 tag 和自定义规则,无需改代码结构。

其实自定义 JSON 解析的核心就是“接管序列化和反序列化的过程”,无论是用 tag 还是实现接口,本质都是根据业务需求调整数据的转换规则。

如果大家有更复杂的场景(比如 JSON 与 protobuf 互转),欢迎在评论区交流~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-json-custom/

备用原文链接: https://blog.fiveyoboy.com/articles/go-json-custom/