Golang使用自定义标签进行JSON的Marshal/Unmarshal操作

huangapple go评论93阅读模式
英文:

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

谢谢 Golang使用自定义标签进行JSON的Marshal/Unmarshal操作

英文:

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 Golang使用自定义标签进行JSON的Marshal/Unmarshal操作

答案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. 你不能向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"}
    }
    
  2. 接下来,你需要为你的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"}
    
  3. 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
    
  4. 既然我们有了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"}
    
  5. 接下来,我们在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]
    
  6. 我们的下一个挑战是访问标签。标签可以使用反射来访问。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
    
  7. 现在,我们将所有的构建块整合到一个工作的解决方案中。唯一值得一提的是创建一个与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
    }
    
  8. 然而,还有一个细节没有完成。使用上面的所有代码,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"}
    }
    

结语

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

  2. 最后,我不得不提到反射可能会很慢,这个示例代码在每个对象上多次使用反射来实现你期望的输出。对于许多用例,以这种方式处理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 {&quot;test&quot;:&quot;Bar&quot;} not {&quot;foo&quot;:&quot;test&quot;} as I think your example would imply.
Here is that code running in the Go Playground to illustrate the output:

package main

import (
    &quot;encoding/json&quot;
    &quot;fmt&quot;
)
type Foo struct {
    Bar string `json:&quot;test&quot; es:&quot;bar&quot;`
}
func main() {
    data, _ := json.Marshal(Foo{&quot;Bar&quot;})
    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 {&quot;bar&quot;:&quot;Bar&quot;} 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):

  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 := &amp;Foo{&quot;Bar&quot;}
        data, _ := f.MarshalWithESTag()
        log.Println(string(data)) // -&gt; {&quot;bar&quot;:&quot;Bar&quot;}
    }
    
  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.<br><br>The following is a simple example that hard-codes a return value of {&quot;hello&quot;:&quot;goodbye&quot;} so you can see in the playground how adding a MarshalJSON() to Foo affects json.Marshal(Foo{&quot;Bar&quot;}):

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

    The output for this will be:

    {&quot;hello&quot;:&quot;goodbye&quot;}
    
  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.<br><br> 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!<br><br>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.
    <br><br>To illustrate, here you can see an example that uses esFoo and sets its Bar property to &quot;baz&quot; giving us output of {&quot;test&quot;:&quot;baz&quot;} (you can also see it run in the Go playground):

    type esFoo Foo
    func (f Foo) MarshalJSON() ([]byte, error) {
        es := esFoo(f)
        es.Bar = &quot;baz&quot;
        _json,err := json.Marshal(es)
        return _json,err
    }
    

    The output for this will be:

    {&quot;test&quot;:&quot;baz&quot;}
    
  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.<br><br>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 (
        &quot;encoding/json&quot;
        &quot;fmt&quot;
    )
    type Car struct {
        Maker string `json:&quot;maker&quot; es:&quot;fabricante&quot;`
        Model string `json:&quot;model&quot; es:&quot;modelo&quot;`
        Year  int    `json:&quot;year&quot;  es:&quot;a&#241;o&quot;`    
    }
    var car = Car{
        Maker:&quot;Chevrolet&quot;,
        Model:&quot;Corvette&quot;,
        Year:2021,
    }
    
    func main() {
        _json,_ := json.Marshal(car)
        var intf interface{}
        _ = json.Unmarshal(_json, &amp;intf)
        m := intf.(map[string]interface{})		
        fmt.Printf(&quot;%v&quot;,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.
    <br><br>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(&quot;es&quot;), 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&lt;t.NumField();i++{
            tag, _ := t.Field(i).Tag.Lookup(&quot;es&quot;)
            fmt.Printf(&quot;%s\n&quot;,tag)
        }
    }
    

    The output for this will be:

    fabricante
    modelo
    a&#241;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, &amp;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(&quot;es&quot;)
                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.
    <br><br>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:
    <br><br>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 (
        &quot;encoding/json&quot;
        &quot;log&quot;
        &quot;reflect&quot;
    )
    
    type Foo struct {
        Foo string `json:&quot;test&quot; es:&quot;bar&quot;`
        Bar string `json:&quot;live&quot; es:&quot;baz&quot;`
    }
    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, &amp;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(&quot;es&quot;)
                if !found {
                    continue
                }
                _m[tag] = v
                i++
            }
            _json,err = json.Marshal(_m)
        }
    end:
        return _json,err
    }
    
    func main()  {
        f := &amp;Foo{&quot;Hello&quot;,&quot;World&quot;}
        data, _ := json.Marshal(f)
        log.Println(string(data)) // -&gt; {&quot;test&quot;:&quot;Hello&quot;,&quot;live&quot;:&quot;World&quot;}
        data, _ = f.MarshalWithESTag()
        log.Println(string(data)) // -&gt; {&quot;bar&quot;:&quot;Hello&quot;,&quot;baz&quot;:&quot;World&quot;}
    }
    

Epilogue

  1. 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.

  2. 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.

huangapple
  • 本文由 发表于 2016年1月7日 01:08:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/34638717.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定