使用自定义标签进行Golang json的编组/解组

6

我想使用自定义标签来进行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。谢谢 :)

你不能直接这样做(但你可以实现自己的UnmarshalJSON方法),为什么? - JimB
1
基本上,我有几个结构体需要作为 JSON(它是一个 API)提供并存储在 Elasticsearch 中。由于 ES 使用 JSON 对象,我希望(并且需要)能够忽略一些字段/更改它们的名称。 - Max Wayt
1
我认为如果你正在编写自定义标签,JSON不是正确的包。我建议创建一个JSON包的包装器,并在其中处理任何自定义标签/函数。如果您查看代码并尝试为您的目的进行反向工程,则可以看到如何对MongoDB BSON结构执行此操作:https://godoc.org/gopkg.in/mgo.v2/bson#Marshal - Verran
1
请参见https://dev59.com/rYTba4cB1Zd3GeqP5F1U,该链接涉及忽略JSON标记的编组问题。 - Charlie Tumahai
@CodingPickle 这里的重点是我想要保留两个标签的marshal / unmarshal函数。 - Max Wayt
显示剩余2条评论
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上查看 —— 之后我会解释这段代码。 (如果我的假设不正确,我也会进行说明):

  1. You cannot add a MarshalWithESTag() method to the the json package because Go does not allow for safe monkey patching. However, you can add a MarshalWithESTag() method to your Foo 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"}
    }
    
  2. Next you need to add a MarshalJSON() method to your Foo struct. This will get called when you call json.Marshal() and pass an instance of Foo to it.

    The following is a simple example that hard-codes a return value of {"hello":"goodbye"} so you can see in the playground how adding a MarshalJSON() to Foo affects json.Marshal(Foo{"Bar"}):

    func (f Foo) MarshalJSON() ([]byte, error) {
        return []byte(`{"hello":"goodbye"}`),nil
    }
    

    The output for this will be:

    {"hello":"goodbye"}
    
  3. Inside the MarshalJSON() method we need to produce JSON with the es tags instead of the json 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.

    And the easiest way to generate JSON in Go is to use json.Marshal(). However, if we use json.Marshal(f) where f is an instance of Foo that gets passed as the receiver when calling MarshalJson() it will end up in an infinite recursive loop!

    The solution is to create a new struct type based on and identical to the existing type of Foo, except for its identity. Creating a new type esFoo based on Foo is as easy as:

    type esFoo Foo
    
  4. Since we have esFoo we can now cast our instance of Foo to be of type esFoo to break the association with our custom MarshalJSON(). This works because our method was specific to the type with the identity of Foo and not with the type esFoo. Passing an instance of esFoo to json.Marshal() allows us to use the default JSON marshalling we get from Go.

    To illustrate, here you can see an example that uses esFoo and sets its Bar 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"}
    
  5. Next we process and manipulate the JSON inside MarshalJSON(). This can be done by using json.Unmarshal() to an interface{} variable which we can then use a type assertion to treat the variable as a map.

    Here is a standalone example unrelated to the prior examples that illustrates this by printing map[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]
    
  6. Our next challenge is to access the tags. Tags are accessible using Reflection. Go provides reflection functionality in the standard reflect package.

    Using our Car struct from above, here is a simple example that illustrates how to use Reflection. It uses the reflect.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 is t.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
    
  7. 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 as m to allow us to store the values using the es 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
    }
    
  8. However, there is still one detail left undone. With all the above code f.MarshalWithESTag() will generate JSON for the es tags, but so will json.Marshal(f) and we want the latter to return its use of the json tags.

    So address that we just need to:

    a. Add a local package variable useESTags with an initial value of false,

    b. Modify f.MarshalWithESTag() to set useESTags to true before calling json.Marshal(), and then

    c. To set useESTags back to false before returning, and

    d. Lastly modify MarshalJSON() to only perform the logic required for the es tags if useESTags is set to true:

    Which brings us to the final code — with a second property in Foo 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"}
    }
    

结语

  1. 如果我的假设是错误的,我认为我至少可以假设我提供的代码足以帮助你实现你的目标。如果你想要交换输出中的键和值,你应该能够根据所展示的技术实现。如果不能,请留言寻求帮助。

  2. 最后,我必须提醒一下,反射可能会很慢,而本例子每个对象都使用了多次反射来实现你所需的输出。对于许多用例,以这种方式处理JSON所需的时间不会很长。然而,对于许多其他用例,执行时间可能是一个致命问题。有几个人评论说你应该采用不同的方法;如果性能很重要和/或使用更符合惯例的Go方法很重要,你可能需要认真考虑他们的建议。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接