基于场景的 Go JSON 字段过滤器。它让同一个 struct 通过 json tag 复用到多个接口响应里,而不是为每个接口维护一份单独的 DTO。
Scene-based JSON field filtering for Go. Reuse one model across multiple API responses with json tags while staying compatible with encoding/json.
很多项目里,同一个模型会在不同接口里返回不同字段:
- 文章列表只需要
uid、nickname、avatar - 个人中心还要返回
sex、vip_end_time、price - 管理后台可能又是一套字段
传统做法通常是:
- 新建很多响应结构体
- 手动拷贝字段
- 或者直接把原始 struct 全量返回
json-filter 的做法是把“哪些字段在哪些场景下返回”直接写在 json tag 里,然后用 SelectScenes / OmitScenes 在运行时生成最终 JSON。
- 支持
struct、map、slice、array、指针、interface{} - 支持深层嵌套组合
- 支持
select(...)、omit(...)、omitempty、func(...) - 支持调用侧一次传入多个场景
- 支持列表响应直接传入
[]struct - 支持
$any - 支持匿名字段展开
- 兼容
encoding/json - 支持自定义
json.Marshaler/encoding.TextMarshaler - Go 版本要求:
1.17+
go get github.com/liu-cn/json-filterpackage main
import (
"encoding/json"
"fmt"
"time"
"github.com/liu-cn/json-filter/filter"
)
type User struct {
UID uint `json:"uid,select(article)"`
Avatar string `json:"avatar,select(article)"`
Nickname string `json:"nickname,select(article|profile)"`
Sex int `json:"sex,select(profile)"`
VipEndTime time.Time `json:"vip_end_time,select(profile)"`
Price string `json:"price,select(profile)"`
}
func main() {
user := User{
UID: 1,
Avatar: "avatar",
Nickname: "boyan",
Sex: 1,
VipEndTime: time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC),
Price: "999.9",
}
article, _ := json.Marshal(filter.SelectScenes(user, "article"))
fmt.Println(string(article))
// {"avatar":"avatar","nickname":"boyan","uid":1}
fmt.Println(filter.SelectScenes(user, "profile"))
// {"nickname":"boyan","price":"999.9","sex":1,"vip_end_time":"2026-04-01T00:00:00Z"}
}有两个入口层级:
SelectScenes(value, scenes...)/OmitScenes(value, scenes...)直接返回一个可以交给json.Marshal、gin.Context.JSON的值。适合接口直接返回。SelectScenesFilter(value, scenes...)/OmitScenesFilter(value, scenes...)返回 typed 的Filter,适合你还想继续取JSON、Bytes、Map、Slice、Interface。
推荐约定:
- 直接响应 HTTP:优先用
SelectScenes/OmitScenes - 需要继续处理过滤结果:优先用
SelectScenesFilter/OmitScenesFilter
filter.SelectScenes(value, scenes...)
filter.OmitScenes(value, scenes...)
filter.SelectScenesFilter(value, scenes...)
filter.OmitScenesFilter(value, scenes...)Filter 提供这些方法:
JSON() (string, error)MustJSON() stringBytes() ([]byte, error)MustBytes() []byteInterface() interface{}Map() map[string]interface{}Slice() []interface{}
说明:
Map()只适合顶层结果是对象时使用Slice()只适合顶层结果是数组时使用- 如果顶层结果可能是标量或
null,优先用Interface()、JSON()、Bytes()
调用侧可以一次传多个场景,语义是“命中任意一个场景就保留/排除”。
场景可以表达接口形态、客户端差异、业务模块、字段包、登录态等不同维度:
type Article struct {
ID int `json:"id,select(summary|detail|admin)"`
Title string `json:"title,select(summary|detail|seo|admin)"`
Cover string `json:"cover,select(summary|mobile)"`
Body string `json:"body,select(detail)"`
MetaTitle string `json:"meta_title,select(seo)"`
InternalNote string `json:"internal_note,select(admin)"`
}
// 列表页:轻量摘要字段
filter.SelectScenes(article, "summary")
// 移动端详情页:详情字段 + 移动端额外素材
filter.SelectScenes(article, "detail", "mobile")
// 后台详情页:详情字段 + 内部字段
filter.SelectScenes(article, "detail", "admin")实际接口里,场景通常来自请求上下文、客户端、开关或登录态,可以直接展开 []string:
type RequestContext struct {
Client string
NeedSEO bool
IsStaff bool
}
func articleScenes(ctx RequestContext) []string {
scenes := []string{"detail"}
if ctx.Client == "mobile" {
scenes = append(scenes, "mobile")
}
if ctx.NeedSEO {
scenes = append(scenes, "seo")
}
if ctx.IsStaff {
scenes = append(scenes, "admin")
}
return scenes
}
ctx := RequestContext{Client: "mobile", NeedSEO: true}
article := Article{
ID: 1,
Title: "Release notes",
Cover: "cover.png",
Body: "Full article",
MetaTitle: "Release notes SEO",
}
fmt.Println(filter.SelectScenes(article, articleScenes(ctx)...))
// {"body":"Full article","cover":"cover.png","id":1,"meta_title":"Release notes SEO","title":"Release notes"}struct 切片也可以直接传进去,列表接口不需要额外循环:
users := []User{
{ID: 1, Name: "Ada", Email: "ada@example.com"},
{ID: 2, Name: "Grace", Email: "grace@example.com"},
}
fmt.Println(filter.SelectScenes(users, "public", "member"))
// [{"email":"ada@example.com","id":1,"name":"Ada"},{"email":"grace@example.com","id":2,"name":"Grace"}]如果使用 profile.age 这种精确字段名作为场景,嵌套 struct 的父字段也需要声明对应场景,过滤器才会继续解析子字段:
type Profile struct {
Age int `json:"age,select(profile.age)"`
Email string `json:"email,select(profile.email)"`
}
type User struct {
Profile Profile `json:"profile,select(profile.age|profile.email)"`
}只在指定场景下输出该字段。
Name string `json:"name,select(article|profile)"`在指定场景下排除该字段。
Password string `json:"password,omit(profile|admin)"`零值时忽略该字段。
Nickname string `json:"nickname,omitempty,select(profile)"`
Avatar *string `json:"avatar,omitempty,select(profile)"`
Age int `json:"age,omitempty,select(profile)"`在过滤时调用当前 struct 上的零参数方法,用方法返回值替代字段值。
Avatar string `json:"avatar,select(profile),func(BuildAvatar)"`注意:
- 方法是定义在“当前字段所属的 struct”上的
- 如果方法是指针接收器,请给
SelectScenes/OmitScenes传指针
表示任意场景都匹配。
UID uint `json:"uid,select($any)"`
Password string `json:"password,omit($any)"`字段名留空时,会把匿名嵌入结构体展开到当前层级。
type Page struct {
PageInfo int `json:"page_info,select(article)"`
}
type Article struct {
Title string `json:"title,select(article)"`
Page `json:",select(article)"`
}输出会类似:
{"page_info":1,"title":"hello"}如果你写成:
Page Page `json:"page,select(article)"`那它会作为普通嵌套对象输出,而不会展开。
这几条语义建议先了解清楚:
select模式下,只会保留带jsontag 且命中select(...)的字段omit模式下,字段默认保留;没有jsontag 的字段也会保留,并使用 struct 字段名json:"-"始终忽略nil指针、nil元素会按null处理- 顶层结果可以是对象、数组、标量或
null map[bool]...、数字 key map、字符串 key map 都支持- 自定义
json.Marshaler/encoding.TextMarshaler会按叶子值处理
type User struct {
Name string `json:"name"`
Password string `json:"password,omit($any)"`
Phone string `json:"phone,omit(public)"`
}
user := User{
Name: "boyan",
Password: "123456",
Phone: "18800000000",
}
fmt.Println(filter.OmitScenes(user, "public"))
// {"name":"boyan"}type Image struct {
URL string `json:"url,select(api),func(BuildURL)"`
Name string
Ext string
}
func (i Image) BuildURL() string {
return i.Name + i.Ext
}
fmt.Println(filter.SelectScenes(Image{
Name: "avatar",
Ext: ".png",
}, "api"))
// {"url":"avatar.png"}f := filter.SelectScenesFilter(user, "profile")
jsonStr, err := f.JSON()
if err != nil {
panic(err)
}
fmt.Println(jsonStr)
bs, err := f.Bytes()
if err != nil {
panic(err)
}
fmt.Println(string(bs))
payload := f.Interface()
_ = payloadfunc GetUser(c *gin.Context) {
user := User{
UID: 1,
Avatar: "avatar",
Nickname: "boyan",
Sex: 1,
}
c.JSON(200, filter.SelectScenes(user, "profile"))
}适合:
- 中小型后端项目
- 同一模型复用到多个响应场景
- 想快速减少 DTO 重复定义
不太适合:
- 极度追求零反射开销的链路
- 想把返回结构显式写死在类型系统里
- 团队不希望把接口规则写进 tag
- 当前模块
go.mod为go 1.17 - 与标准库
encoding/json配合使用 - 老 API 仍保留兼容,但新代码建议优先使用
SelectScenes/OmitScenes:Select(scene, value)/Omit(scene, value)SelectFilter(scene, value)/OmitFilter(scene, value)SelectMarshal/OmitMarshalMustMarshalJSONMastMarshalJSON
新代码建议优先使用:
SelectScenes/OmitScenesSelectScenesFilter/OmitScenesFilterBytes/MustBytes
可以直接看仓库里的示例和测试:
example/filter/example_test.gofilter/filter_test.go
json-filter is a scene-based JSON field filter for Go. It lets one struct serve
multiple API responses by encoding scene rules directly in json tags.
go get github.com/liu-cn/json-filterGo version: 1.17+
type User struct {
UID uint `json:"uid,select(article)"`
Avatar string `json:"avatar,select(article)"`
Nickname string `json:"nickname,select(article|profile)"`
Sex int `json:"sex,select(profile)"`
}
fmt.Println(filter.SelectScenes(user, "article"))
// {"avatar":"avatar","nickname":"boyan","uid":1}SelectScenes(value, scenes...)/OmitScenes(value, scenes...)Use these when you want to pass the result directly tojson.Marshalor an HTTP framework response helper.SelectScenesFilter(value, scenes...)/OmitScenesFilter(value, scenes...)Use these when you want the typedFilterhelpers such asJSON,Bytes,Map,Slice, orInterface.
Multiple requested scenes use OR semantics: a field is included or excluded when any requested scene matches its tag.
Scenes can describe response shapes, client-specific fields, feature modules, reusable field bundles, login state, and other business dimensions:
type Article struct {
ID int `json:"id,select(summary|detail|admin)"`
Title string `json:"title,select(summary|detail|seo|admin)"`
Cover string `json:"cover,select(summary|mobile)"`
Body string `json:"body,select(detail)"`
MetaTitle string `json:"meta_title,select(seo)"`
InternalNote string `json:"internal_note,select(admin)"`
}
filter.SelectScenes(article, "summary")
filter.SelectScenes(article, "detail", "mobile")
filter.SelectScenes(article, "detail", "admin")In real handlers, scenes often come from request context, client type, feature
flags, or login state. Pass that []string with variadic expansion:
type RequestContext struct {
Client string
NeedSEO bool
IsStaff bool
}
func articleScenes(ctx RequestContext) []string {
scenes := []string{"detail"}
if ctx.Client == "mobile" {
scenes = append(scenes, "mobile")
}
if ctx.NeedSEO {
scenes = append(scenes, "seo")
}
if ctx.IsStaff {
scenes = append(scenes, "admin")
}
return scenes
}
ctx := RequestContext{Client: "mobile", NeedSEO: true}
article := Article{
ID: 1,
Title: "Release notes",
Cover: "cover.png",
Body: "Full article",
MetaTitle: "Release notes SEO",
}
fmt.Println(filter.SelectScenes(article, articleScenes(ctx)...))
// {"body":"Full article","cover":"cover.png","id":1,"meta_title":"Release notes SEO","title":"Release notes"}Slices can be passed directly too, so list responses do not need an extra loop:
users := []User{
{ID: 1, Name: "Ada", Email: "ada@example.com"},
{ID: 2, Name: "Grace", Email: "grace@example.com"},
}
fmt.Println(filter.SelectScenes(users, "public", "member"))
// [{"email":"ada@example.com","id":1,"name":"Ada"},{"email":"grace@example.com","id":2,"name":"Grace"}]select(article|profile)include in these scenesomit(admin|internal)exclude in these scenesomitemptydrop zero valuesfunc(BuildValue)call a zero-arg method on the containing struct$anymatch every scene
- In
selectmode, only fields with matchingselect(...)tags are included. - In
omitmode, fields are included by default unless excluded. json:"-"is always ignored.- Structs, maps, slices, arrays, pointers, interfaces, and nested combinations are supported.
- Custom
json.Marshalerandencoding.TextMarshalerleaf values are supported. - Legacy
Select(scene, value),Omit(scene, value),SelectFilter(scene, value), andOmitFilter(scene, value)remain supported for compatibility, but new code should prefer the value-firstSelectScenesandOmitScenesAPIs.