英文:
Golang Marshal/Unmarshal json with a custom tag
问题
我想要使用自定义标签来编组/解组 Golang 对象(json)。
像这样:
type Foo struct {
Bar string `json:"test" es:"bar"`
}
data, _ := json.MarshalWithESTag(Foo{"Bar"})
log.Println(string(data)) // -> {"foo":"bar"}
换句话说,我想在这里使用 encoding/json 库的不同标签:https://github.com/golang/go/blob/master/src/encoding/json/encode.go#L1033
谢谢
英文:
I want to Marshal / Unmarshal Golang object (json) with a custom tag.
Like
type Foo struct {
Bar string `json:"test" es:"bar"`
}
data, _ := json.MarshalWithESTag(Foo{"Bar"})
log.Println(string(data)) // -> {"foo":"bar"}
In other words, I whan to use the encoding/json library with a different tag here: https://github.com/golang/go/blob/master/src/encoding/json/encode.go#L1033
Thanks
答案1
得分: 6
我认为你编写示例的方式可能有些不正确。
当我使用Marshal()
替换MarshalWithESTag()
运行你的代码时,我得到的结果是{"test":"Bar"}
,而不是你的示例中暗示的{"foo":"test"}
。
这里是在Go Playground中运行该代码以说明输出的示例:
package main
import (
"encoding/json"
"fmt"
)
type Foo struct {
Bar string `json:"test" es:"bar"`
}
func main() {
data, _ := json.Marshal(Foo{"Bar"})
fmt.Println(string(data))
}
如果我的理解是正确的,那么你真正想要的是在调用json.MarshalWithESTag()
时输出为{"bar":"Bar"}
。
基于这个假设,你可以使用以下代码实现,你可以在Go Playground中查看这段代码,之后我会解释代码的含义。(如果我的假设不正确,我也会解决这个问题):
-
你不能向
json
包中添加MarshalWithESTag()
方法,因为Go不允许进行安全的 猴子补丁。然而,你可以向你的Foo
结构体添加一个MarshalWithESTag()
方法,这个示例还展示了如何调用它:func (f Foo) MarshalWithESTag() ([]byte, error) { data, err := json.Marshal(f) return data,err } func main() { f := &Foo{"Bar"} data, _ := f.MarshalWithESTag() log.Println(string(data)) // -> {"bar":"Bar"} }
-
接下来,你需要为你的
Foo
结构体添加一个MarshalJSON()
方法。当你调用json.Marshal()
并将Foo
的实例传递给它时,它将被调用。<br><br>以下是一个简单的示例,它将返回值硬编码为{"hello":"goodbye"}
,这样你就可以看到将MarshalJSON()
添加到Foo
中如何影响json.Marshal(Foo{"Bar"})
的输出:func (f Foo) MarshalJSON() ([]byte, error) { return []byte(`{"hello":"goodbye"}`),nil }
这将输出:
{"hello":"goodbye"}
-
在
MarshalJSON()
方法内部,我们需要生成具有es
标签的JSON,而不是json
标签,这意味着我们需要在方法内部生成JSON,因为Go不会为我们提供JSON;它期望我们生成它。<br><br>在Go中生成JSON的最简单方法是使用json.Marshal()
。然而,如果我们使用json.Marshal(f)
,其中f
是传递给MarshalJson()
时的Foo
的实例,它将陷入无限递归循环!<br><br>解决方案是基于现有类型创建一个新的结构体类型,该类型与Foo
的现有类型完全相同,除了其标识。创建一个基于Foo
的新类型esFoo
非常简单:type esFoo Foo
-
既然我们有了
esFoo
,我们现在可以将Foo
的实例强制转换为esFoo
类型,以打破与我们自定义的MarshalJSON()
的关联。这样做的原因是我们的方法特定于具有Foo
_标识_的类型,而不是esFoo
类型。将esFoo
的实例传递给json.Marshal()
允许我们使用Go提供的默认JSON编组。
<br><br>为了说明这一点,这里有一个示例,它使用esFoo
并将其Bar
属性设置为"baz"
,给我们输出{"test":"baz"}
(你也可以在Go Playground中看到它运行):type esFoo Foo func (f Foo) MarshalJSON() ([]byte, error) { es := esFoo(f) es.Bar = "baz" _json,err := json.Marshal(es) return _json,err }
这将输出:
{"test":"baz"}
-
接下来,我们在
MarshalJSON()
中处理和操作JSON。可以通过使用json.Unmarshal()
将其解组到一个interface{}
变量中来完成这个操作,然后我们可以使用类型断言将变量视为map。<br><br>下面是一个独立的示例,与前面的示例无关,它通过打印map[maker:Chevrolet model:Corvette year:2021]
来说明这一点_(同样,你可以在Go Playground中看到它运行)_:package main import ( "encoding/json" "fmt" ) type Car struct { Maker string `json:"maker" es:"fabricante"` Model string `json:"model" es:"modelo"` Year int `json:"year" es:"año"` } var car = Car{ Maker:"Chevrolet", Model:"Corvette", Year:2021, } func main() { _json,_ := json.Marshal(car) var intf interface{} _ = json.Unmarshal(_json, &intf) m := intf.(map[string]interface{}) fmt.Printf("%v",m) }
这将输出:
map[maker:Chevrolet model:Corvette year:2021]
-
我们的下一个挑战是访问标签。标签可以使用反射来访问。Go在标准的reflect包中提供了反射功能。
<br><br>使用上面的Car
结构体,这里有一个简单的示例,说明如何使用反射。它使用reflect.TypeOf()
函数将类型作为值检索出来,然后检查该类型以获取每个字段的标签。检索每个标签的代码是t.Field(i).Tag.Lookup("es")
,希望这个代码相对容易理解_(同样,你可以在Go Playground中查看它的运行情况)_:func main() { t := reflect.TypeOf(car) for i:=0; i<t.NumField();i++{ tag, _ := t.Field(i).Tag.Lookup("es") fmt.Printf("%s\n",tag) } }
这将输出:
fabricante modelo año
-
现在,我们将所有的构建块整合到一个工作的解决方案中。唯一值得一提的是创建一个与
m
长度相同的新映射变量_m
,以便我们可以使用es
标签存储值:func (f Foo) MarshalJSON() ([]byte, error) { es := esFoo(f) _json,err := json.Marshal(es) { if err != nil { goto end } var intf interface{} err = json.Unmarshal(_json, &intf) if err != nil { goto end } m := intf.(map[string]interface{}) _m := make(map[string]interface{},len(m)) t := reflect.TypeOf(f) i := 0 for _,v := range m { tag, found := t.Field(i).Tag.Lookup("es") if !found { continue } _m[tag] = v i++ } _json,err = json.Marshal(_m) } end: return _json,err }
-
然而,还有一个细节没有完成。使用上面的所有代码,
f.MarshalWithESTag()
将为es
标签生成JSON,但是json.Marshal(f)
也会生成相同的JSON,我们希望后者返回其使用json
标签的结果。
<br><br>为了解决这个问题,我们只需要:a. 添加一个初始值为
false
的局部包变量useESTags
,b. 修改
f.MarshalWithESTag()
在调用json.Marshal()
之前将useESTags
设置为true
,然后c. 在返回之前将
useESTags
设置回false
,然后d. 最后修改
MarshalJSON()
,只有在useESTags
设置为true
时才执行与es
标签相关的逻辑:
<br><br>这就是最终的代码了,Foo
中还添加了第二个属性以提供更好的示例_(最后,你当然可以在Go Playground中看到这里)_:package main import ( "encoding/json" "log" "reflect" ) type Foo struct { Foo string `json:"test" es:"bar"` Bar string `json:"live" es:"baz"` } type esFoo Foo var useESTags = false func (f Foo) MarshalWithESTag() ([]byte, error) { useESTags = true data, err := json.Marshal(f) useESTags = false return data,err } func (f Foo) MarshalJSON() ([]byte, error) { es := esFoo(f) _json,err := json.Marshal(es) if useESTags { if err != nil { goto end } var intf interface{} err = json.Unmarshal(_json, &intf) if err != nil { goto end } m := intf.(map[string]interface{}) _m := make(map[string]interface{},len(m)) t := reflect.TypeOf(f) i := 0 for _,v := range m { tag, found := t.Field(i).Tag.Lookup("es") if !found { continue } _m[tag] = v i++ } _json,err = json.Marshal(_m) } end: return _json,err } func main() { f := &Foo{"Hello","World"} data, _ := json.Marshal(f) log.Println(string(data)) // -> {"test":"Hello","live":"World"} data, _ = f.MarshalWithESTag() log.Println(string(data)) // -> {"bar":"Hello","baz":"World"} }
结语
-
如果我的假设是错误的,我认为我至少可以假设我提供的代码足以实现你的目标。根据所示的技术,你应该能够交换输出中的键和值,如果这实际上是你想要的话。如果不是,请在评论中提出问题寻求帮助。
-
最后,我不得不提到反射可能会很慢,这个示例代码在每个对象上多次使用反射来实现你期望的输出。对于许多用例,以这种方式处理JSON所需的时间不会很长。然而,对于许多其他用例,执行时间可能是一个致命因素。有几个评论说你应该以不同的方式来解决这个问题;如果性能很重要和/或使用更符合Go惯例的方法很重要,你可能需要认真考虑他们的建议。
英文:
I think the way you wrote your example might have been a bit incorrect?
When I run your code using Marshal()
inplace of MarshalWithESTag()
I get {"test":"Bar"}
not {"foo":"test"}
as I think your example would imply.
Here is that code running in the Go Playground to illustrate the output:
package main
import (
"encoding/json"
"fmt"
)
type Foo struct {
Bar string `json:"test" es:"bar"`
}
func main() {
data, _ := json.Marshal(Foo{"Bar"})
fmt.Println(string(data))
}
Assuming I am correct about what you wanted then that would imply what you really wanted was for your output to be {"bar":"Bar"}
when you call json.MarshalWithESTag()
.
Based on that assumption you could accomplish with the following code — which you can see in the Go Playground — after which I will explain the code. (If my assumption was not correct I will address that too):
-
You cannot add a
MarshalWithESTag()
method to the thejson
package because Go does not allow for safe monkey patching. However, you can add aMarshalWithESTag()
method to yourFoo
struct, and this example also shows you how to call it:func (f Foo) MarshalWithESTag() ([]byte, error) { data, err := json.Marshal(f) return data,err } func main() { f := &Foo{"Bar"} data, _ := f.MarshalWithESTag() log.Println(string(data)) // -> {"bar":"Bar"} }
-
Next you need to add a
MarshalJSON()
method to yourFoo
struct. This will get called when you calljson.Marshal()
and pass an instance ofFoo
to it.<br><br>The following is a simple example that hard-codes a return value of{"hello":"goodbye"}
so you can see in the playground how adding aMarshalJSON()
toFoo
affectsjson.Marshal(Foo{"Bar"})
:func (f Foo) MarshalJSON() ([]byte, error) { return []byte(`{"hello":"goodbye"}`),nil }
The output for this will be:
{"hello":"goodbye"}
-
Inside the
MarshalJSON()
method we need to produce JSON with thees
tags instead of thejson
tags meaning we will need to generate JSON within the method because Go does not provide us with the JSON; it expects us to generate it.<br><br> And the easiest way to generate JSON in Go is to usejson.Marshal()
. However, if we usejson.Marshal(f)
wheref
is an instance ofFoo
that gets passed as the receiver when callingMarshalJson()
it will end up in an infinite recursive loop!<br><br>The solution is to create a new struct type based on and identical to the existing type ofFoo
, except for its identity. Creating a new typeesFoo
based onFoo
is as easy as:type esFoo Foo
-
Since we have
esFoo
we can now cast our instance ofFoo
to be of typeesFoo
to break the association with our customMarshalJSON()
. This works because our method was specific to the type with the identity ofFoo
and not with the typeesFoo
. Passing an instance ofesFoo
tojson.Marshal()
allows us to use the default JSON marshalling we get from Go.
<br><br>To illustrate, here you can see an example that usesesFoo
and sets itsBar
property to"baz"
giving us output of{"test":"baz"}
(you can also see it run in the Go playground):type esFoo Foo func (f Foo) MarshalJSON() ([]byte, error) { es := esFoo(f) es.Bar = "baz" _json,err := json.Marshal(es) return _json,err }
The output for this will be:
{"test":"baz"}
-
Next we process and manipulate the JSON inside
MarshalJSON()
. This can be done by usingjson.Unmarshal()
to aninterface{}
variable which we can then use a type assertion to treat the variable as a map.<br><br>Here is a standalone example unrelated to the prior examples that illustrates this by printingmap[maker:Chevrolet model:Corvette year:2021]
(Again you can see it work in the Go Playground):package main import ( "encoding/json" "fmt" ) type Car struct { Maker string `json:"maker" es:"fabricante"` Model string `json:"model" es:"modelo"` Year int `json:"year" es:"año"` } var car = Car{ Maker:"Chevrolet", Model:"Corvette", Year:2021, } func main() { _json,_ := json.Marshal(car) var intf interface{} _ = json.Unmarshal(_json, &intf) m := intf.(map[string]interface{}) fmt.Printf("%v",m) }
The output for this will be:
map[maker:Chevrolet model:Corvette year:2021]
-
Our next challenge is to access the tags. Tags are accessible using Reflection. Go provides reflection functionality in the standard reflect package.
<br><br>Using ourCar
struct from above, here is a simple example that illustrates how to use Reflection. It uses thereflect.TypeOf()
function to retrieve the type as a value and then introspects that type to retrieve the tags for each field. The code for retrieving each tag ist.Field(i).Tag.Lookup("es")
, which is hopefully somewhat self-explanatory (and again, check it out in the Go Playground):func main() { t := reflect.TypeOf(car) for i:=0; i<t.NumField();i++{ tag, _ := t.Field(i).Tag.Lookup("es") fmt.Printf("%s\n",tag) } }
The output for this will be:
fabricante modelo año
-
Now that we have covered all the building blocks we can bring it all together into a working solution. The only addition worth mentioning are the creation of a new map variable
_m
of the same length asm
to allow us to store the values using thees
tags:func (f Foo) MarshalJSON() ([]byte, error) { es := esFoo(f) _json,err := json.Marshal(es) { if err != nil { goto end } var intf interface{} err = json.Unmarshal(_json, &intf) if err != nil { goto end } m := intf.(map[string]interface{}) _m := make(map[string]interface{},len(m)) t := reflect.TypeOf(f) i := 0 for _,v := range m { tag, found := t.Field(i).Tag.Lookup("es") if !found { continue } _m[tag] = v i++ } _json,err = json.Marshal(_m) } end: return _json,err }
-
However, there is still one detail left undone. With all the above code
f.MarshalWithESTag()
will generate JSON for thees
tags, but so willjson.Marshal(f)
and we want the latter to return its use of thejson
tags.
<br><br>So address that we just need to:a. Add a local package variable
useESTags
with an initial value offalse
,b. Modify
f.MarshalWithESTag()
to setuseESTags
totrue
before callingjson.Marshal()
, and thenc. To set
useESTags
back tofalse
before returning, andd. Lastly modify
MarshalJSON()
to only perform the logic required for thees
tags ifuseESTags
is set totrue
:
<br><br>Which brings us to the final code — with a second property inFoo
to provide a better example (and finally, you can of course see here in the Go Playground):package main import ( "encoding/json" "log" "reflect" ) type Foo struct { Foo string `json:"test" es:"bar"` Bar string `json:"live" es:"baz"` } type esFoo Foo var useESTags = false func (f Foo) MarshalWithESTag() ([]byte, error) { useESTags = true data, err := json.Marshal(f) useESTags = false return data,err } func (f Foo) MarshalJSON() ([]byte, error) { es := esFoo(f) _json,err := json.Marshal(es) if useESTags { if err != nil { goto end } var intf interface{} err = json.Unmarshal(_json, &intf) if err != nil { goto end } m := intf.(map[string]interface{}) _m := make(map[string]interface{},len(m)) t := reflect.TypeOf(f) i := 0 for _,v := range m { tag, found := t.Field(i).Tag.Lookup("es") if !found { continue } _m[tag] = v i++ } _json,err = json.Marshal(_m) } end: return _json,err } func main() { f := &Foo{"Hello","World"} data, _ := json.Marshal(f) log.Println(string(data)) // -> {"test":"Hello","live":"World"} data, _ = f.MarshalWithESTag() log.Println(string(data)) // -> {"bar":"Hello","baz":"World"} }
Epilogue
-
If my assumption was wrong I think I can at least assume this code I provided gives you enough to achieve your objective. You should be able to swap the keys and values in your output if that is actually what you want given the techniques shown. If not, please comment asking for help.
-
Finally, I would be remiss not to mention that reflection can be slow and that this example uses reflection multiple times per object to achieve your desired output. For many use-cases the time required to process JSON this way won't be significant. However, for many other use-cases the execution time can be a deal-killer. Several commented that you should approach this a different way; if performance matters and/or using a more idiomatic Go approach is important, you might want to seriously consider their recommendations.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论