Compare commits

...

14 Commits

Author SHA1 Message Date
fc340b9417 Merge pull request '已知问题修复' (#7) from fetature/fix_parent_tag into master
Reviewed-on: #7
2025-04-29 14:58:00 +08:00
141a376d19 修复嵌套对象json tag设置错误问题 2025-04-29 14:57:15 +08:00
d139b409fd 修复数组未判断长度导致索引越界的BUG 2025-04-29 11:37:13 +08:00
7e464374bb remove debug code 2025-04-28 21:11:18 +08:00
6b9942a175 Merge pull request '支持从json动态生成结构体,并转化为指定格式' (#6) from feature/support_json into master
Reviewed-on: #6
2025-04-28 21:05:25 +08:00
e4c0f9abf3 数组支持递归解析 2025-04-28 18:18:04 +08:00
b4932bf7cd 支持通过配置传入XmlName, 默认值 XmlData 2025-04-28 17:40:40 +08:00
f9024b9498 支持从json -> xml的转换 2025-04-28 17:34:23 +08:00
cd85d80da6 Merge pull request '未指定tag时, 默认支持 json/xml/yaml/toml/ini' (#5) from feature/upgrade_tag into master
Reviewed-on: #5
2025-04-28 10:42:09 +08:00
103f3b38ba 未指定tag时, 默认支持 json/xml/yaml/toml/ini 2025-04-28 10:41:42 +08:00
5d4788e0fd 更新README 2025-04-01 15:35:34 +08:00
c8d891c438 update go mod 2025-04-01 14:41:13 +08:00
1b217c0e4f Merge pull request '支持传入的跟结构就是数组' (#4) from feature/fix_root_array into master
Reviewed-on: #4
2025-03-23 20:55:06 +08:00
bc91937116 支持传入的跟结构就是数组 2025-03-23 20:54:35 +08:00
10 changed files with 364 additions and 69 deletions

113
README.md
View File

@ -1,5 +1,7 @@
# Golang 运行时动态结构体
## 简介
主要提供在运行时动态生成结构体的能力, 同时支持合并多个已有结构体,生成一个新的结构体
主要功能如下:
@ -11,53 +13,82 @@
* 修改已存在字段的类型以及Tag标签
* 读取动态结构体字段的Helper
* 动态结构体的值, 解析到一个已定义的结构体中
* 动态结构体生成slice或者map实例
* 动态结构体生成slice或者map实例
* 支持嵌套的结构体
* 支持N维数组
* 支持嵌套的结构体数组
## 开发背景
开发个人网关项目时, 面临一个问题: **`如何验证网关请求参数的合法性?`** 在Golang生态中,比较知名的参数验证库是 [https://github.com/go-playground/validator](go-playground/validator/v10)。 其支持的参数验证方式如下:
- 单个值按照指定规则进行验证, 无上下文信息
- map数据验证, 无上下文信息, 可以看做是单个值的验证的批量版本
- 结构体数据验证, 支持上下文信息, 可以跨结构体字段进行关联验证
网关参数验证的需求背景如下:
- 基础的参数验证, 如取值范围、长度等
- 数据格式校验IP地址、URL、邮箱、手机号等
- 数据关联校验两个字段的值必须相等、A字段不存在,则B字段必须存在等
- ......
综合数据验证库提供的验证能力, 以及业务场景的复杂性, 需要使用结构体数据进行参数验证。那么, 此时则面临另外一个问题: 不同的网关接口是有不同的参数结构的, 如何在运行时动态生成结构体呢? 这就需要用到本文所介绍的动态结构体生成能力。
## 快速开始
```bash
# 安装
go get -v -u github.com/liuxiaobopro/golang-dynamic-struct
```
## 使用示例
```go
package main
import (
"encoding/json"
"fmt"
"log"
"git.zhangdeman.cn/zhangdeman/dynamic-struct"
dynamicstruct "git.zhangdeman.cn/zhangdeman/dynamic-struct"
"github.com/go-playground/validator/v10"
"fmt"
)
func main() {
instance := NewStruct().
AddField("Integer", "", 0, `json:"int"`, false).
AddField("Text", "", "", `json:"someText"`, false).
AddField("Float", "", 0.0, `json:"double"`, false).
AddField("Boolean", "", false, "", false).
AddField("Slice", "", []int{}, "", false).
AddField("Anonymous", "", "", `json:"-"`, false).
Build().
New()
data := []byte(`
{
"int": 123,
"someText": "example",
"double": 123.45,
"Boolean": true,
"Slice": [1, 2, 3],
"Anonymous": "avoid to read"
}
`)
err := json.Unmarshal(data, &instance)
if err != nil {
log.Fatal(err)
}
data, err = json.Marshal(instance)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// Out:
// {"int":123,"someText":"example","double":123.45,"Boolean":true,"Slice":[1,2,3]}
// 动态生成一个结构体
ds := dynamicstruct.NewStruct()
ds = ds.AddField("name", "", `json:"name" validate:"required"`)
ds = ds.AddField("age", 0, `json:"age" validate:"gte=0,lte=130"`)
ds = ds.AddField("email", "", `json:"email" validate:"required,email"`)
// 生成结构体实例
user := ds.Build().New()
userTestData := `{
"name": "张三",
"age": 180,
"email": "test@qq.com",
}`
validatorInstance = validator.New()
validatorInstance.SetTagName("validate")
err := validatorInstance.Struct(user)
fmt.Println(err)
}
```
## 实现原理
无论任何语言, 但凡出现 **`动态`** 这一类字眼, 实现原理一定脱离不了 **`反射`** , 且 **`反射是核心实现技术`** , 本库中的实现原理也不例外, 也是基于反射进行实现的。核心逻辑是利用 **`reflect.New`** 这一泛实例化数据的机制进行实现. 确定原理方向, **`抽象的需求运行时动态生成不定结构的结构体`** 已经转化为具体功能诉求, **`如何利用reflect.New这一机制, 动态实例化结构体`**
## 具体实现
核心就是一句代码:
```go
structFields := []reflect.StructField
reflect.New(reflect.StructOf(structFields))
```
其中, **`structFields`** 是一个 **`一个结构体字段配置列表,其可配置的属性具体查看即可`**, **`reflect.StructOf`** 则是根据结构体字段配置反射出结构体类型, 利用 **`reflect.New`** 这一机制, 可以动态实例化一个 **`结构体`**。
核心逻辑就是这么简单, 但是, 动态生成结构体的过程中, 还需要考虑到很多其他的问题, 如:
- 如何动态生成嵌套的结构体
- 如何动态生成嵌套的结构体数组
- 如何动态生成N维数组
- 如何动态生成结构体数组
其实上述逻辑无非是基于最原始的基础逻辑, **`递归进行实现`** , 所以, 具体实现细节, 可以查看源码。

View File

@ -1,12 +1,13 @@
package dynamicstruct
import (
"fmt"
"git.zhangdeman.cn/zhangdeman/wrapper"
"reflect"
"strings"
)
const tagTpl = `json:"{TAG_NAME}" xml:"{TAG_NAME}" toml:"{TAG_NAME}" yaml:"{TAG_NAME}" ini:"{TAG_NAME}"`
type nestedStruct struct {
Field string
Builder IBuilder
@ -67,7 +68,10 @@ func (b *builderImpl) AddField(name string, pkg string, typ any, tag string, ano
tag = cfgTag
} else {
if len(tag) == 0 {
tag = fmt.Sprintf(`json:"%s"`, name)
arr := strings.Split(strings.TrimSuffix(name, ".[]"), ".")
// 没指定tag, 字段名称作为tag名称
tag = strings.ReplaceAll(tagTpl, "{TAG_NAME}", arr[len(arr)-1])
// tag = fmt.Sprintf(`json:"%s"`, name)
}
}
// 判断是否嵌套结构体
@ -118,7 +122,8 @@ func (b *builderImpl) addNestedField(nameArr []string, pkg string, typ any, tag
parentName := strings.Join(nameArr[:i], ".")
parentJsonTag := nameArr[i-1]
parentFieldName := wrapper.String(parentJsonTag).SnakeCaseToCamel()
fieldTag := fmt.Sprintf(`json:"%s"`, parentJsonTag)
// fieldTag := fmt.Sprintf(`json:"%s"`, parentJsonTag)
fieldTag := strings.ReplaceAll(tagTpl, "{TAG_NAME}", parentJsonTag)
if tagCfg, exist := b.structTagTable[parentName]; exist && len(tagCfg) > 0 {
fieldTag = tagCfg
}
@ -178,6 +183,9 @@ func (b *builderImpl) Build() IDynamicStruct {
for deep := b.maxFieldDeep - 1; deep > 0; deep-- {
// 嵌套数据结构
for parentIndex, builderCfg := range b.nestedStructTable {
if parentIndex == ArrayRootFlag {
continue
}
parentNameArr := strings.Split(parentIndex, ".")
if len(parentNameArr) != deep {
// 从深度最深处向上处理
@ -201,24 +209,40 @@ func (b *builderImpl) Build() IDynamicStruct {
}
if arrDeep > 0 {
// 数组嵌套数组配置
val := reflect.ValueOf(builderCfg.Builder.Build().New()).Elem().Interface()
val := reflect.ValueOf(builderCfg.Builder.Build().New()).Interface()
for i := 0; i < arrDeep; i++ {
val = reflect.New(reflect.SliceOf(reflect.TypeOf(val))).Interface()
val = reflect.New(reflect.SliceOf(reflect.TypeOf(val).Elem())).Interface()
// 删除数组记录
arrPath := strings.Join(parentNameArr[:len(parentNameArr)-i], ".")
delete(b.nestedStructTable, arrPath)
}
// 解除指针引用
// val = reflect.ValueOf(val).Elem().Interface()
newParamIndex := strings.Join(newParentNameArr, ".")
newParentIndex := strings.Join(newParentNameArr, ".")
isTopIndex := len(newParentNameArr) == 1
if isTopIndex {
// 顶层结构, 数组类型不存在还有其他属性情况, 直接追加字段, 并移除嵌套表里的定义
b.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false)
b.AddField(b.nestedStructTable[newParentIndex].Field, "", val, b.nestedStructTable[newParentIndex].Tag, false)
// b.nestedStructTable[newParamIndex].Builder.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false)
} else {
// 非顶层结构, 再上探一级
b.nestedStructTable[strings.Join(newParentNameArr[:len(newParentNameArr)-1], ".")].Builder.AddField(b.nestedStructTable[newParamIndex].Field, "", val, b.nestedStructTable[newParamIndex].Tag, false)
newParentName := ""
fieldName := ""
if len(newParentNameArr) == 0 {
// 说明传入的直接就是数组
newParentName = ArrayRootFlag
fieldName = ArrayRootFlag
b.nestedStructTable[newParentName] = nestedStruct{
Field: fieldName,
Builder: NewStruct(b.structTagTable),
Tag: "",
}
b.nestedStructTable[newParentName].Builder.AddField(b.nestedStructTable[newParentName].Field, "", reflect.ValueOf(val).Elem().Interface(), b.nestedStructTable[newParentName].Tag, false)
} else {
newParentName = strings.Join(newParentNameArr[:len(newParentNameArr)-1], ".")
b.nestedStructTable[newParentName].Builder.AddField(b.nestedStructTable[newParentIndex].Field, "", reflect.ValueOf(val).Elem().Interface(), b.nestedStructTable[newParentIndex].Tag, false)
}
}
// 嵌套结构中删除数组字段
delete(b.nestedStructTable, newParamIndex)
delete(b.nestedStructTable, newParentIndex)
} else {
// 非数组
// (非顶层) 父级结构存在, 将其追加到父级结构中即可, 向前看一步即为父级结构
@ -227,6 +251,15 @@ func (b *builderImpl) Build() IDynamicStruct {
}
}
}
if nestedCfg, exist := b.nestedStructTable[ArrayRootFlag]; exist {
// 说明是传入的根路径就是数组
return &dynamicStructImpl{
structFields: nil,
definition: reflect.TypeOf(nestedCfg.Builder.GetField(ArrayRootFlag).GetType()),
}
}
// 一级字段属性
var structFields []reflect.StructField
for _, field := range b.fields {

View File

@ -223,3 +223,45 @@ func TestExtendStruct(t *testing.T) {
So(instance.GetField("Age").GetTag(), ShouldEqual, `json:"company_age"`)
})
}
func TestArrayRoot(t *testing.T) {
Convey("嵌套结构体带多级数组", t, func() {
data := []byte(`
[[[{
"age": 123,
"name": "example",
"double": 123.45,
"bool": true,
"slice": [1, 2, 3]
}]]]
`)
instance := NewStruct(map[string]string{
"test_tag_map": `json:"tag_map"`,
}).
AddField("[].[].[].age", "", 0, `json:"age"`, false).
AddField("[].[].[].name", "", "", `json:"name"`, false).
Build()
val := instance.New()
err := json.Unmarshal(data, &val)
So(err, ShouldBeNil)
reflectType := reflect.TypeOf(val)
// 需要是数组类型
So(reflectType.Kind(), ShouldEqual, reflect.Ptr)
So(reflectType.Elem().Kind(), ShouldEqual, reflect.Slice)
// 验证数据
// 数据序列化成功
targetByte, targetErr := json.Marshal(val)
So(targetErr, ShouldBeNil)
// 数据转map成功
var res [][][]any
targetUnmarshalErr := json.Unmarshal(targetByte, &res)
So(targetUnmarshalErr, ShouldBeNil)
So(len(res), ShouldEqual, 1)
userMap, ok := res[0][0][0].(map[string]any)
So(ok, ShouldBeTrue)
So(len(userMap), ShouldEqual, 2)
So(userMap["age"], ShouldEqual, float64(123))
So(userMap["name"], ShouldEqual, "example")
So(userMap["double"], ShouldBeNil)
})
}

View File

@ -3,4 +3,6 @@ package dynamicstruct
const (
// ArraySplit is the string used to split array elements
ArraySplit = "[]"
// ArrayRootFlag 传入路径直接以数组开头
ArrayRootFlag = "__RootArrayFlag"
)

View File

@ -7,7 +7,9 @@
// Date : 2025-03-23 14:14
package dynamicstruct
import "reflect"
import (
"reflect"
)
type dynamicStructImpl struct {
structFields []reflect.StructField
@ -16,7 +18,6 @@ type dynamicStructImpl struct {
// New 创建动态结构体实例
func (ds *dynamicStructImpl) New() any {
// 不要指针,全部解引用
return reflect.New(ds.definition).Interface()
}

10
go.mod
View File

@ -3,14 +3,14 @@ module git.zhangdeman.cn/zhangdeman/dynamic-struct
go 1.24.1
require (
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740
github.com/smartystreets/goconvey v1.8.1
)
require (
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8 // indirect
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995 // indirect
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 // indirect
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd // indirect
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250428041157-135850ee8a58 // indirect
git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
@ -22,9 +22,5 @@ require (
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

18
go.sum
View File

@ -1,13 +1,23 @@
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8 h1:VEifPc+vkpEQoX9rj7zxmT1m+IA81XjOxe7+Z1aqWNM=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250227040546-863c03f34bb8/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c h1:cl3gQGXQpJ8ugDs0C/hQLfcvF4lGBm5BeABLvROFDoM=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995 h1:LmPRAf0AsxRVFPibdpZR89ajlsz8hof2IvMMyTqiEq4=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 h1:gUDlQMuJ4xNfP2Abl1Msmpa3fASLWYkNlqDFF/6GN0Y=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd h1:q7GG14qgXKB4MEXQFOe7/UYebsqMfPaSX80TcPdOosI=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd/go.mod h1:+D6uPSljwHywjVY5WSBY4TRVMj26TN5f5cFGEYMldjs=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250426132259-73cf1be49c7f h1:7QgAcGnmVEVyIPeWH0ZkQN/jpzklYXsKCenTR2GpxbE=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250426132259-73cf1be49c7f/go.mod h1:Ig3GZC2hJDkQp7F8Tm53GvMWLh9bdbbauow/vxGO4YA=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250428041157-135850ee8a58 h1:fTkmucGaUoKocoX+ASM4AnwsAVJOtOOLUFSqA+uwVzg=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250428041157-135850ee8a58/go.mod h1:Ig3GZC2hJDkQp7F8Tm53GvMWLh9bdbbauow/vxGO4YA=
git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e h1:Q973S6CcWr1ICZhFI1STFOJ+KUImCl2BaIXm6YppBqI=
git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e/go.mod h1:VpPjBlwz8U+OxZuxzHQBv1aEEZ3pStH6bZvT21ADEbI=
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436 h1:SM4zc54W2wmM72+4pMNQ8iS371H6lj4J8rj8KJKf7pw=
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250302133417-c1588abcb436/go.mod h1:YJ1FlvFgkfAHkxkt3l5rKKUqEpQkNMbCFDzDmgteEU8=
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740 h1:zPUoylfJTbc0EcxW+NEzOTBmoeFZ2I/rLFBnEzxb4Wk=
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740/go.mod h1:1ct92dbVc49pmXusA/iGfcQUJzcYmJ+cjAhgc3sDv1I=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
@ -39,14 +49,6 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

153
wrapper/json.go Normal file
View File

@ -0,0 +1,153 @@
// Package wrapper ...
//
// Description : wrapper ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-04-28 15:32
package wrapper
import (
"encoding/xml"
"errors"
"fmt"
dynamicstruct "git.zhangdeman.cn/zhangdeman/dynamic-struct"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
)
func NewJson(sourceData string, o *Option) (*ownJson, error) {
gjsonRes := gjson.Parse(sourceData)
if gjsonRes.Value() == nil {
return nil, errors.New("source data parse result is nil")
}
// 仅支持map
if !gjsonRes.IsObject() {
return nil, errors.New("source result is not map or struct Marshal string")
}
if nil == o {
o = &Option{}
}
if o.XmlName == "" {
o.XmlName = "XmlData"
}
oj := &ownJson{
sourceData: sourceData,
gjsonResult: gjsonRes,
structBuilder: dynamicstruct.NewStruct(nil),
o: o,
}
if err := oj.GenerateStruct(); nil != err {
return nil, err
}
return oj, nil
}
type ownJson struct {
sourceData string // 数据源
gjsonResult gjson.Result // 数据源解析为gjson
structBuilder dynamicstruct.IBuilder // 结构体构造器
structRes any // 解析的结构体值
o *Option // 一些操作选项
}
// GenerateStruct 生成结构体字段列表
func (oj *ownJson) GenerateStruct() error {
oj.gjsonResult.ForEach(func(fieldName, fieldValue gjson.Result) bool {
oj.generateStructField("", fieldName.String(), fieldValue)
return true
})
// 追加xml的标签
oj.structBuilder.AddField("XMLName", "", xml.Name{}, fmt.Sprintf(`json:"-" toml:"-" yml:"-" init:"-" xml:"%v"`, oj.o.XmlName), false)
val := oj.structBuilder.Build().New()
if err := serialize.JSON.UnmarshalWithNumber([]byte(oj.sourceData), &val); nil != err {
return err
}
oj.structRes = val
return nil
}
// Marshal 结果序列化
func (oj *ownJson) Marshal(marshalType string) ([]byte, error) {
return serialize.Wrapper.Marshal(marshalType, oj.structRes)
}
// generateStructField 递归解析
func (oj *ownJson) generateStructField(rootPath string, currentName string, currentResult gjson.Result) {
structPath := oj.getPath(rootPath, currentName)
if currentResult.IsBool() {
// bool类型
oj.structBuilder.AddField(structPath, "", true, "", false)
return
}
if currentResult.Type == gjson.Number {
// 数字类型, 统一用float64
oj.structBuilder.AddField(structPath, "", float64(0), "", false)
return
}
if currentResult.Value() == nil {
// 空值, any类型
oj.structBuilder.AddField(structPath, "", (*any)(nil), "", false)
return
}
if currentResult.Type == gjson.String {
// 字符串类型
oj.structBuilder.AddField(structPath, "", "", "", false)
return
}
if currentResult.IsObject() {
// 还是一个嵌套对象, 递归处理
currentResult.ForEach(func(key, value gjson.Result) bool {
oj.generateStructField(structPath, key.String(), value)
return true
})
return
}
if currentResult.IsArray() {
// 数组, 递归处理
arrList := currentResult.Array()
if len(arrList) == 0 {
// 空数组,无法判断类型,使用any
oj.structBuilder.AddField(structPath, "", []any{}, "", false)
return
}
if arrList[0].Type == gjson.True || arrList[0].Type == gjson.False {
oj.structBuilder.AddField(structPath, "", []bool{}, "", false)
return
}
if arrList[0].Type == gjson.Number {
oj.structBuilder.AddField(structPath, "", []float64{}, "", false)
return
}
if arrList[0].Type == gjson.String {
oj.structBuilder.AddField(structPath, "", []string{}, "", false)
return
}
if arrList[0].Type == gjson.Null {
oj.structBuilder.AddField(structPath, "", []any{}, "", false)
return
}
if arrList[0].IsArray() {
// 数组递归处理
oj.generateStructField(structPath+".[]", "", arrList[0])
return
}
// 对象结构,递归处理
arrList[0].ForEach(func(key, value gjson.Result) bool {
oj.generateStructField(structPath+".[]", key.String(), value)
return true
})
return
}
}
// getPath 获取路径
func (oj *ownJson) getPath(root string, currentName string) string {
if root == "" {
return currentName
}
if currentName == "" {
return root
}
return root + "." + currentName
}

22
wrapper/json_test.go Normal file
View File

@ -0,0 +1,22 @@
// Package wrapper ...
//
// Description : wrapper ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-04-28 16:59
package wrapper
import (
"fmt"
"testing"
)
func TestNewJson(t *testing.T) {
sourceData := `{"name": "test", "age":18,"company":{"address": "Beijing", "name":"lala"},"index":[1,2,3,4], "deep":[[{"name":"a","age":20},{"name":"c"}], [{"name":"b"},{"name":"d"}]]}`
instance, iErr := NewJson(sourceData, &Option{XmlName: "ResponseData"})
fmt.Println(iErr)
res, err := instance.Marshal("xml")
fmt.Println(err)
fmt.Println(string(res))
}

13
wrapper/option.go Normal file
View File

@ -0,0 +1,13 @@
// Package wrapper ...
//
// Description : wrapper ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-04-28 17:35
package wrapper
// Option 生成结构体之后的可用选项
type Option struct {
XmlName string `json:"xml_name"` // 如果目标格式是 xml, 必须指定xmlName作为根节点
}