在Go中使用匿名成员展平编组的JSON结构

时间:2013-12-03 21:34:39

标签: json go marshalling

给出以下代码:(reproduced here at play.golang.org。)

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}

type Session struct {
    Id     int `json:"id"`
    UserId int `json:"userId"`
}

type Anything interface{}

type Hateoas struct {
    Anything
    Links map[string]string `json:"_links"`
}

func MarshalHateoas(subject interface{}) ([]byte, error) {
    h := &Hateoas{subject, make(map[string]string)}
    switch s := subject.(type) {
    case *User:
        h.Links["self"] = fmt.Sprintf("http://user/%d", s.Id)
    case *Session:
        h.Links["self"] = fmt.Sprintf("http://session/%d", s.Id)
    }
    return json.MarshalIndent(h, "", "    ")
}

func main() {
    u := &User{123, "James Dean"}
    s := &Session{456, 123}
    json, err := MarshalHateoas(u)
    if err != nil {
        panic(err)
    } else {
        fmt.Println("User JSON:")
        fmt.Println(string(json))
    }
    json, err = MarshalHateoas(s)
    if err != nil {
        panic(err)
    } else {
        fmt.Println("Session JSON:")
        fmt.Println(string(json))
    }
}

我试图让渲染的JSON看起来正确在我的情况下,这意味着:

User JSON:
{
    "id": 123,
    "name": "James Dean",
    "_links": {
        "self": "http://user/123"
    }
}
Session JSON:
{
    "id": 456,
    "userId": 123,
    "_links": {
        "self": "http://session/456"
    }
}

不幸的是,Go将匿名成员视为真正的命名事物,因此它采用定义的类型(Anything)并因此命名JSON:

User JSON:
{
    "Anything": {
        "id": 123,
        "name": "James Dean"
    },
    "_links": {
        "self": "http://user/123"
    }
}
Session JSON:
{
    "Anything": {
        "id": 456,
        "userId": 123
    },
    "_links": {
        "self": "http://session/456"
    }
}

来自the docs的JSON中没有关于匿名成员处理的明确文档:

  

匿名结构字段通常被封送,就好像它们的内部导出字段是外部结构中的字段一样,这取决于下一段中描述的通常的Go可见性规则。在其JSON标记中给出名称的匿名结构字段被视为具有该名称,而不是匿名。

     

匿名结构字段的处理是Go 1.1中的新功能。在Go 1.1之前,匿名结构字段被忽略。要强制忽略当前版本和早期版本中的匿名结构字段,请为该字段指定“ - ”的JSON标记。

这并不能说明是否有办法让事情变得平坦,或者向马歇尔勒提示我要做什么。

我确信,有一个特殊情况,可能有魔术名称,它具有重命名XML编组器中XML文档的根元素的特殊含义。

在这种情况下,我也没有以任何方式附加代码,我的用例是拥有一个接受interface{}, *http.Request, http.ResponseWriter的函数并在线路上写回HATEOAS文档,打开传递的类型,推断哪些链接写回JSON。 (从而访问请求,请求主机,端口,方案等,以及类型本身以推断URL和已知字段等)

7 个答案:

答案 0 :(得分:2)

工作场所链接:http://play.golang.org/p/_r-bQIw347

它的要点是这个;通过使用反射包,我们循环遍历我们希望序列化的结构的字段并将它们映射到map[string]interface{}我们现在可以保留原始结构的平面结构而不引入新字段。

告诫者,应该对这段代码中的一些假设进行几次检查。例如,它假设MarshalHateoas始终接收指向值的指针。

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}

type Session struct {
    Id     int `json:"id"`
    UserId int `json:"userId"`
}

func MarshalHateoas(subject interface{}) ([]byte, error) {
    links := make(map[string]string)
    out := make(map[string]interface{})
    subjectValue := reflect.Indirect(reflect.ValueOf(subject))
    subjectType := subjectValue.Type()
    for i := 0; i < subjectType.NumField(); i++ {
        field := subjectType.Field(i)
        name := subjectType.Field(i).Name
        out[field.Tag.Get("json")] = subjectValue.FieldByName(name).Interface()
    }
    switch s := subject.(type) {
    case *User:
        links["self"] = fmt.Sprintf("http://user/%d", s.Id)
    case *Session:
        links["self"] = fmt.Sprintf("http://session/%d", s.Id)
    }
    out["_links"] = links
    return json.MarshalIndent(out, "", "    ")
}
func main() {
    u := &User{123, "James Dean"}
    s := &Session{456, 123}
    json, err := MarshalHateoas(u)
    if err != nil {
        panic(err)
    } else {
        fmt.Println("User JSON:")
        fmt.Println(string(json))
    }
    json, err = MarshalHateoas(s)
    if err != nil {
        panic(err)
    } else {
        fmt.Println("Session JSON:")
        fmt.Println(string(json))
    }
}

答案 1 :(得分:0)

使用 omitempty 标签和一些逻辑,这样你就可以使用一种类型,为不同的情况产生正确的输出。

诀窍是知道JSON编码器何时将值视为空。来自encoding / json文档:

  

空值为false,0,任何nil指针或接口值,以及   任何长度为零的数组,切片,映射或字符串。

这是您的程序稍作修改,以产生您想要的输出。当它们的值为“空”时,它会忽略某些字段 - 具体来说,JSON编码器将省略以“0”作为值的整数并且映射为零长度。

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Id     int               `json:"id"`
    Name   string            `json:"name,omitempty"`
    UserId int               `json:"userId,omitempty"`
    Links  map[string]string `json:"_links,omitempty"`
}

func Marshal(u *User) ([]byte, error) {
    u.Links = make(map[string]string)
    if u.UserId != 0 {
        u.Links["self"] = fmt.Sprintf("http://user/%d", u.UserId)
    } else if u.Id != 0 {
        u.Links["self"] = fmt.Sprintf("http://session/%d", u.Id)
    }
    return json.MarshalIndent(u, "", "    ")
}

func main() {
    u := &User{Id: 123, Name: "James Dean"}
    s := &User{Id: 456, UserId: 123}
    json, err := Marshal(u)
    if err != nil {
        panic(err)
    } else {
        fmt.Println(string(json))
    }
    json, err = Marshal(s)
    if err != nil {
        panic(err)
    } else {
        fmt.Println(string(json))
    }
}

Copy on play.golang.org.

答案 2 :(得分:0)

我经历过的一种方法涉及在类型上使用方法。有关详细信息,请参阅http://play.golang.org/p/bPWB4ryDQn

基本上,您从相反的角度处理问题 - 而不是将基类型“封装”为Hateoas类型,而是在每个基类型中包含所需的映射。然后,在每个基类型上实现一个方法,该方法负责相应地更新Links字段。

这会产生预期的结果,并且只有边缘源代码样板。

{
    "id": 123,
    "name": "James Dean",
    "_links": {
        "self": "http://user/123"
    }
}
{
    "id": 456,
    "userId": 123,
    "_links": {
        "self": "http://session/456"
    }
}

我相信除此之外的任何其他方式,特别是如果你坚持使用嵌入和扩展方法,将需要实现自定义封送器(http://golang.org/pkg/encoding/json/#Marshaler),并且可能还需要使用反射包,尤其是因为任何东西都是interface {}。

答案 3 :(得分:0)

这有点棘手。但有一点可以肯定的是,该文档通过以下方式处理您的样本:

  

接口值编码为接口

中包含的值
来自同一link

所以没有其他事可做。 “Anything”虽然它是匿名的,但它是一个接口变量,所以我可能会期待你样本中的行为。

我带了your code并进行了一些更改。 This示例有效,但有一些副作用。在这种情况下,我需要更改ID成员名称,以便发生nome碰撞。但后来我也改变了json标签。如果我没有更改标签,那么代码似乎需要花费太长时间才能运行,并且重复的标签都被省略了。 (here)。

PS:我不能肯定地说,但我猜这个假设存在问题。我会假设我要去Marshal的任何东西都希望能够UnMarshal。你的扁平化者不会这样做。如果你想要一个flattener你可能需要分叉JSON编码器并为标签添加一些值(就像有一个'omitempty'你可能会添加'flatten'。

答案 4 :(得分:0)

抱歉,我认为您尝试生成的JSON不是有效的JSON对象,因此可能是JsonMarshal不与您玩游戏的原因。

该对象可能无法通过JavaScript消耗,因为它包含两个对象,除非您将对象包装在一个数组中。

[
    {
        "id": 123,
        "name": "James Dean",
        "_links": {
            "self": "http://user/123"
        }
    },
    {
        "id": 456,
        "userId": 123,
        "_links": {
            "self": "http://session/456"
        }
    }
]

然后你可以使用这个JSON,例如:

var user, session;
user = jsonString[0]; 
session = jsonString[1];

考虑给出对象根名称可能是一个更好的考虑因素,例如:

{
    "user": {
        "id": 123,
        "name": "James Dean",
        "_links": {
            "self": "http://user/123"
        }
    },
    "session": {
        "id": 456,
        "userId": 123,
        "_links": {
            "self": "http://session/456"
        }
    }
}

并消费为例如:

var user, session;
user = jsonString.user;
session = jsonString.session;

我希望这有助于你

答案 5 :(得分:0)

我需要做类似的事情并尝试使用嵌入式界面的技术,并且(毫不奇怪)有同样的问题。

我不想改变所有可能需要额外字段的结构,但最终我已经妥协了这个解决方案:

http://play.golang.org/p/asLFPx76jw

package main

import (
    "encoding/json"
    "fmt"
)

type HateoasFields struct {
    Links []interface{} `json:"_links,omitempty"`
}

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
    HateoasFields
}

type Session struct {
    Id     int `json:"id"`
    UserId int `json:"userId"`
    HateoasFields
}

func main() {
    u := &User{Id: 123, Name: "James Dean"}
    s := &Session{Id: 456, UserId: 123}

    u.Links = []interface{}{fmt.Sprintf("http://user/%d", u.Id)}
    s.Links = []interface{}{fmt.Sprintf("http://session/%d", s.Id)}

    uBytes, _ := json.Marshal(u)
    sBytes, _ := json.Marshal(s)

    fmt.Println(string(uBytes))
    fmt.Println(string(sBytes))
}

哪个输出:

{"id":123,"name":"James Dean","_links":["http://user/123"]}
{"id":456,"userId":123,"_links":["http://session/456"]}

不是将原始结构嵌入到带有附加字段的结构中,而是反过来。

在查看原始代码时,我不认为这个解决方案非常棒。这比将Links属性添加到原始结构更好吗?但对于我的应用程序需求,这是最好的事情。

答案 6 :(得分:0)

如果你只想担心地图,我喜欢这个解决方案。

// Flatten takes a map and returns a new one where nested maps are replaced
// by dot-delimited keys.
func Flatten(m map[string]interface{}) map[string]interface{} {
    o := make(map[string]interface{})
    for k, v := range m {
            switch child := v.(type) {
            case map[string]interface{}:
                    nm := Flatten(child)
                    for nk, nv := range nm {
                            o[k+"."+nk] = nv
                    }
            default:
                    o[k] = v
            }
    }
    return o
}