Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

125 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

json-filter

基于场景的 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.

中文

这库解决什么问题

很多项目里,同一个模型会在不同接口里返回不同字段:

  • 文章列表只需要 uidnicknameavatar
  • 个人中心还要返回 sexvip_end_timeprice
  • 管理后台可能又是一套字段

传统做法通常是:

  • 新建很多响应结构体
  • 手动拷贝字段
  • 或者直接把原始 struct 全量返回

json-filter 的做法是把“哪些字段在哪些场景下返回”直接写在 json tag 里,然后用 SelectScenes / OmitScenes 在运行时生成最终 JSON。

特性

  • 支持 structmapslicearray、指针、interface{}
  • 支持深层嵌套组合
  • 支持 select(...)omit(...)omitemptyfunc(...)
  • 支持调用侧一次传入多个场景
  • 支持列表响应直接传入 []struct
  • 支持 $any
  • 支持匿名字段展开
  • 兼容 encoding/json
  • 支持自定义 json.Marshaler / encoding.TextMarshaler
  • Go 版本要求:1.17+

安装

go get github.com/liu-cn/json-filter

1 分钟上手

package 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.Marshalgin.Context.JSON 的值。适合接口直接返回。
  • SelectScenesFilter(value, scenes...) / OmitScenesFilter(value, scenes...) 返回 typed 的 Filter,适合你还想继续取 JSONBytesMapSliceInterface

推荐约定:

  • 直接响应 HTTP:优先用 SelectScenes / OmitScenes
  • 需要继续处理过滤结果:优先用 SelectScenesFilter / OmitScenesFilter

核心 API

filter.SelectScenes(value, scenes...)
filter.OmitScenes(value, scenes...)

filter.SelectScenesFilter(value, scenes...)
filter.OmitScenesFilter(value, scenes...)

Filter 提供这些方法:

  • JSON() (string, error)
  • MustJSON() string
  • Bytes() ([]byte, error)
  • MustBytes() []byte
  • Interface() 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)"`
}

Tag 规则

select(...)

只在指定场景下输出该字段。

Name string `json:"name,select(article|profile)"`

omit(...)

在指定场景下排除该字段。

Password string `json:"password,omit(profile|admin)"`

omitempty

零值时忽略该字段。

Nickname string  `json:"nickname,omitempty,select(profile)"`
Avatar   *string `json:"avatar,omitempty,select(profile)"`
Age      int     `json:"age,omitempty,select(profile)"`

func(...)

在过滤时调用当前 struct 上的零参数方法,用方法返回值替代字段值。

Avatar string `json:"avatar,select(profile),func(BuildAvatar)"`

注意:

  • 方法是定义在“当前字段所属的 struct”上的
  • 如果方法是指针接收器,请给 SelectScenes / OmitScenes 传指针

$any

表示任意场景都匹配。

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 模式下,只会保留带 json tag 且命中 select(...) 的字段
  • omit 模式下,字段默认保留;没有 json tag 的字段也会保留,并使用 struct 字段名
  • json:"-" 始终忽略
  • nil 指针、nil 元素会按 null 处理
  • 顶层结果可以是对象、数组、标量或 null
  • map[bool]...、数字 key map、字符串 key map 都支持
  • 自定义 json.Marshaler / encoding.TextMarshaler 会按叶子值处理

示例

omit 示例

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"}

func(...) 示例

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()
_ = payload

在 Gin 中使用

func GetUser(c *gin.Context) {
	user := User{
		UID:      1,
		Avatar:   "avatar",
		Nickname: "boyan",
		Sex:      1,
	}

	c.JSON(200, filter.SelectScenes(user, "profile"))
}

什么时候适合用它

适合:

  • 中小型后端项目
  • 同一模型复用到多个响应场景
  • 想快速减少 DTO 重复定义

不太适合:

  • 极度追求零反射开销的链路
  • 想把返回结构显式写死在类型系统里
  • 团队不希望把接口规则写进 tag

兼容性说明

  • 当前模块 go.modgo 1.17
  • 与标准库 encoding/json 配合使用
  • 老 API 仍保留兼容,但新代码建议优先使用 SelectScenes / OmitScenes
    • Select(scene, value) / Omit(scene, value)
    • SelectFilter(scene, value) / OmitFilter(scene, value)
    • SelectMarshal / OmitMarshal
    • MustMarshalJSON
    • MastMarshalJSON

新代码建议优先使用:

  • SelectScenes / OmitScenes
  • SelectScenesFilter / OmitScenesFilter
  • Bytes / MustBytes

更多示例

可以直接看仓库里的示例和测试:

  • example/
  • filter/example_test.go
  • filter/filter_test.go

English

What It Does

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.

Install

go get github.com/liu-cn/json-filter

Go version: 1.17+

Quick Example

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}

Recommended API

  • SelectScenes(value, scenes...) / OmitScenes(value, scenes...) Use these when you want to pass the result directly to json.Marshal or an HTTP framework response helper.
  • SelectScenesFilter(value, scenes...) / OmitScenesFilter(value, scenes...) Use these when you want the typed Filter helpers such as JSON, Bytes, Map, Slice, or Interface.

Multiple Scenes

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"}]

Tag Syntax

  • select(article|profile) include in these scenes
  • omit(admin|internal) exclude in these scenes
  • omitempty drop zero values
  • func(BuildValue) call a zero-arg method on the containing struct
  • $any match every scene

Notes

  • In select mode, only fields with matching select(...) tags are included.
  • In omit mode, fields are included by default unless excluded.
  • json:"-" is always ignored.
  • Structs, maps, slices, arrays, pointers, interfaces, and nested combinations are supported.
  • Custom json.Marshaler and encoding.TextMarshaler leaf values are supported.
  • Legacy Select(scene, value), Omit(scene, value), SelectFilter(scene, value), and OmitFilter(scene, value) remain supported for compatibility, but new code should prefer the value-first SelectScenes and OmitScenes APIs.

License

MIT

About

golang json字段过滤,复用struct 随意选择你想要输出为json的结构体字段。 json filter Golang's JSON filter randomly selects the structure fields you want to output as JSON,Let go have dynamic language like json processing capability

Topics

Resources

Stars

134 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages