Skip to content

自定义编码

json 库与标准库 encoding/json 保持编码兼容,因此自定义类型的 JSON 形态主要通过实现标准库接口来完成。本页介绍当前版本实际生效的编码扩展点:

接口优先

对「某个类型如何编码」的需求,优先实现 MarshalJSONMarshalText;这类实现可被本库、标准库 encoding/json 以及任何兼容库共用,可移植性最强。

json.Marshaler 接口

实现 MarshalJSON() ([]byte, error) 的类型可以完全决定自己的 JSON 表示。库在编码时会优先调用该方法(值接收者与指针接收者均支持),与标准库 encoding/json 行为一致。

接口签名(与 encoding/json.Marshaler 兼容):

go
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

下面定义一个 Hex 类型,把 uint64 编码为带 0x 前缀的十六进制字符串:

go
package main

import (
	"fmt"
	"strconv"

	"github.com/cybergodev/json"
)

// Hex 将 uint64 包装为十六进制表示的类型。
type Hex uint64

// MarshalJSON 实现 json.Marshaler,将数值编码为 "0x.." 字符串。
func (h Hex) MarshalJSON() ([]byte, error) {
	return []byte(`"0x` + strconv.FormatUint(uint64(h), 16) + `"`), nil
}

func main() {
	type Device struct {
		ID    Hex    `json:"id"`
		Label string `json:"label"`
	}
	d := Device{ID: Hex(255), Label: "sensor-1"}

	out, err := json.Marshal(d)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// 输出:{"id":"0xff","label":"sensor-1"}
}

避免无限递归

MarshalJSON 内部若需要「常规编码」辅助,请使用标准库 stdjson.Marshal 或针对一个不同的具体类型调用本库。直接对本类型再次调用 Marshal 会重新进入 MarshalJSON,形成无限递归。

encoding.TextMarshaler 接口

未实现 MarshalJSON 但实现了 MarshalText() ([]byte, error) 的类型,会被编码为以文本内容为值的 JSON 字符串(自动补引号与转义)。适合用文本即可完整表达形态的类型。

接口签名(与 encoding.TextMarshaler 兼容):

go
type TextMarshaler interface {
    MarshalText() ([]byte, error)
}

下面定义一个 Slug 类型,编码时自动规范化为小写连字符形式:

go
package main

import (
	"fmt"
	"strings"

	"github.com/cybergodev/json"
)

// Slug 表示 URL 友好的短文本。
type Slug string

// MarshalText 实现 encoding.TextMarshaler,输出规范化后的文本。
func (s Slug) MarshalText() ([]byte, error) {
	return []byte(strings.ToLower(strings.ReplaceAll(string(s), " ", "-"))), nil
}

func main() {
	type Article struct {
		Title string `json:"title"`
		Slug  Slug   `json:"slug"`
	}
	a := Article{Title: "Hello World", Slug: Slug("Hello World")}

	out, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// 输出:{"title":"Hello World","slug":"hello-world"}
}

两种接口的优先级

同一类型若同时实现两个接口,MarshalJSON 优先于 MarshalText。若需要把类型编码为 JSON 字符串,实现 MarshalText 通常更简洁(无需自行处理引号与转义)。

time.Time 的内置处理

库对 time.Time 做了内置处理,统一输出 RFC3339Nano 格式(保留亚秒精度,与标准库 encoding/json 一致)。无需任何配置:

go
package main

import (
	"fmt"
	"time"

	"github.com/cybergodev/json"
)

func main() {
	type Event struct {
		Name string    `json:"name"`
		At   time.Time `json:"at"`
	}
	t := time.Date(2026, 1, 15, 10, 30, 0, 0, time.UTC)
	e := Event{Name: "deploy", At: t}

	out, err := json.Marshal(e)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// 输出:{"name":"deploy","at":"2026-01-15T10:30:00Z"}
}

若需要不同的时间格式,为该类型实现 MarshalJSON(见上文)即可覆盖内置行为——自定义类型的 MarshalJSON 总是优先于 time.Time 的默认处理。

自定义字符转义 CustomEscapes

Config.CustomEscapes 是一个 map[rune]string,用于全局覆盖某些字符的转义方式。编码字符串时,库会先查这个映射:命中则把对应的字符串原样写入输出(你需自行保证其 JSON 合法性),未命中才走默认转义。

下面把版权符号 © 改写为 ASCII 文本(命中即原样写入,其余字符走默认处理):

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	cfg := json.DefaultConfig()
	// © 默认原样输出;这里改写为 ASCII 文本
	cfg.CustomEscapes = map[rune]string{
		'©': "(c)",
	}

	out, err := json.EncodeWithConfig(map[string]string{"note": "Copyright © 2026"}, cfg)
	if err != nil {
		panic(err)
	}
	fmt.Println(out)
	// 输出:{"note":"Copyright (c) 2026"}
}

自定义转义串必须 JSON 合法

CustomEscapes 的值会被原样写入输出,不再二次处理,因此要当心 Go 源码自身的字符串转义:若希望输出中带字面的反斜杠转义序列,Go 源码里需写双反斜杠 \\(写单反斜杠会被 Go 当作转义处理,得到的是该字符本身而非转义序列)。

何时触发自定义转义路径

设置 CustomEscapes(非 nil)即激活自定义编码路径。该路径同时也会读取 EscapeHTMLEscapeUnicodeEscapeSlashEscapeNewlinesEscapeTabsSortKeysFloatPrecisionIncludeNulls 等字段(详见配置选项)。

如何选择扩展点

需求使用方式
某个类型自定义其 JSON 形态实现 MarshalJSON()
某个类型编码为 JSON 字符串(文本表达)实现 MarshalText()
全局改变某些字符的转义规则Config.CustomEscapes
控制缩进、HTML 转义、Unicode 转义、键排序、浮点精度等ConfigPretty/EscapeHTML/EscapeUnicode/SortKeys/FloatPrecision 等字段(见配置选项
覆盖 time.Time 的默认时间格式给自定义时间类型实现 MarshalJSON()

Config 中生效的编码相关字段

字段类型说明
CustomEscapesmap[rune]string自定义字符转义映射(命中即原样输出)
EscapeHTMLbool是否转义 < > &(默认 true
EscapeUnicodebool是否将 >0x7F 的字符转义为 \uXXXX
EscapeSlashbool是否转义 /
EscapeNewlines / EscapeTabsbool是否转义换行/制表符
SortKeysbool是否对对象键排序(对象键默认已排序)
FloatPrecisionint浮点精度(-1 为默认)
IncludeNullsbool是否包含空值字段

未连接的扩展字段(预留)

未连接的扩展字段

Config.CustomEncoderCustomEncoder 接口)与 Config.CustomTypeEncodersTypeEncoder 接口)在当前版本中已声明并参与配置克隆与缓存键计算,但尚未在编码流水线中挂接。设置这两个字段不会改变编码输出。它们是为未来版本预留的扩展点;在此之前,请使用上文 MarshalJSON/MarshalText/CustomEscapes 等已生效的机制。

go
// 当前版本:以下两个字段已声明但未挂接,设置后无效果(预留接口)
type CustomEncoder interface {
    Encode(value any) (string, error)
}

type TypeEncoder interface {
    Encode(v reflect.Value) (string, error)
}

相关