【问题标题】:Golang Marshal/Unmarshal json with a custom tag带有自定义标签的 Golang Marshal/Unmarshal json
【发布时间】:2016-01-06 17:08:49
【问题描述】:

我想用自定义标签编组/解组 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 方法),为什么?
  • 基本上我有几个结构需要用作 json(它是一个 API)并存储在 Elasticsearch 中。由于 ES 使用的是 json 对象,我希望(并且需要)能够忽略某些字段/更改它们的名称。
  • 如果您正在编写自定义标签,我认为 JSON 不适合使用。我会说为 JSON 包创建一个包装器并处理其中的任何自定义标签/函数。如果您查看代码并尝试为您的目的对其进行逆向工程,您可以看到它是如何为 MongoDB BSON 结构完成的:godoc.org/gopkg.in/mgo.v2/bson#Marshal
  • @Verran 我已经考虑过“分叉”bson 库来做我想做的事情,我认为这将是我的解决方案

标签: json go


【解决方案1】:

我认为您编写示例的方式可能有点不正确?

当我使用Marshal() 代替MarshalWithESTag() 运行您的代码时,我得到{"test":"Bar"} 而不是{"foo":"test"},正如我认为您的示例所暗示的那样。 Here 是在 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 中的 you can see)来完成,之后我将解释代码。 (如果我的假设不正确,我也会解决):

  1. 您不能将 MarshalWithESTag() 方法添加到 json 包,因为 Go 不允许 safe monkey patching。但是,您可以MarshalWithESTag() 方法添加到您的Foo 结构中,此示例还向您展示了如何调用它:

    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. 接下来,您需要将 MarshalJSON() method 添加到您的 Foo 结构中。当您调用 json.Marshal() 并将 Foo 的实例传递给它时,它将被调用。

    以下是一个简单的示例,它硬编码了 {"hello":"goodbye"} 的返回值,所以 you can see in the playground 如何添加MarshalJSON()Foo 会影响 json.Marshal(Foo{"Bar"})

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

    输出将是:

    {"hello":"goodbye"}
    
  3. MarshalJSON() 方法中,我们需要使用es tags 而不是json 标签生成JSON,这意味着我们需要在该方法中生成JSON,因为Go 没有为我们提供JSON;它希望我们生成它。

    在 Go 中生成 JSON 的最简单方法是使用 json.Marshal()。但是,如果我们使用json.Marshal(f),其中fFoo 的一个实例,在调用MarshalJson() 时会以receiver 的形式传递,最终会陷入无限递归循环!

    解决方案是基于Foo 的现有类型并且与Foo 相同的create a new struct type,除了它的标识。基于Foo 创建一个新类型esFoo 非常简单:

    type esFoo Foo
    
  4. 由于我们有esFoo,我们现在可以将Foo 的实例转换为esFoo 类型,以打破与我们自定义MarshalJSON() 的关联。这是有效的,因为我们的方法特定于具有Fooidentity 类型而不是esFoo 类型。将 esFoo 的实例传递给 json.Marshal() 允许我们使用从 Go 获得的默认 JSON 编组。

    为了说明,在这里您可以看到一个使用 esFoo 并将其 Bar 属性设置为 "baz" 的示例,从而为我们提供 {"test":"baz"} 的输出 (您也可以在去游乐场):

    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。 This can be done 通过使用 json.Unmarshal()interface{} 变量,然后我们可以使用 type assertion 将变量视为 map

    这是一个独立的示例,与前面的示例无关通过打印map[maker:Chevrolet model:Corvette year:2021] 来说明这一点(同样你可以在 Go Playground 中 see it work):

    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. 我们的下一个挑战是访问标签。可以使用Reflection 访问标签。 Go 在标准 reflect 包中提供反射功能。

    使用上面的 Car 结构,这里有一个简单的例子来说明如何使用反射。它使用reflect.TypeOf() 函数将类型作为值检索,然后自省该类型以检索每个字段的标签。检索每个标签的代码是 t.Field(i).Tag.Lookup("es"),希望这有点不言自明(同样是在 Go Playground 中的 check it out):

    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 标签的使用。

    所以我们只需要解决:

    一个。添加一个初始值为false的本地包变量useESTags

    b.在调用json.Marshal()之前修改f.MarshalWithESTag(),将useESTags设置为true,然后

    c。在返回之前将useESTags 设置回false,并且

    d。最后修改MarshalJSON() 以仅在useESTags 设置为true 时执行es 标签所需的逻辑:

    这将我们带到了最终代码——Foo 中的第二个属性以提供更好的示例(最后,您当然可以在 Go Playground 中使用 see here):

    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 所需的时间并不重要。但是,对于许多其他用例来说,执行时间可能会成为交易杀手。一些人评论说你应该以不同的方式处理这个问题;如果性能很重要和/或使用更idiomatic Go 的方法很重要,您可能需要认真考虑他们的建议。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-15
    • 1970-01-01
    • 2018-10-15
    • 1970-01-01
    • 2015-05-13
    相关资源
    最近更新 更多