在Go语言中,如何展开具有匿名成员的序列化JSON结构体

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

Flattening marshalled JSON structs with anonymous members in Go

问题

给定以下代码:(在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"
    }
}

关于匿名成员在 JSON 中的处理,文档中并没有明确的说明,来自文档

> 匿名结构字段通常被编组为如果它们的内部导出字段是外部结构体的字段,遵循通常的 Go 可见性规则,如下一段所述。具有在其 JSON 标签中给出的名称的匿名结构字段被视为具有该名称,而不是匿名的。
>
> 对匿名结构字段的处理是在 Go 1.1 中引入的。在 Go 1.1 之前,匿名结构字段被忽略。要在当前版本和早期版本中强制忽略匿名结构字段,请给字段一个 JSON 标签为“-”。

这并没有明确说明是否有一种方法可以展开或提示编组器我想要做什么。

我相信可能有一种方法,因为在 XML 编组器中有一个特殊情况,魔术名称具有重命名 XML 文档根元素的特殊含义。

在这种情况下,我对代码没有任何依赖,我的用例是编写一个接受 interface{}, *http.Request, http.ResponseWriter 的函数,并通过网络传输 HATEOAS 文档,根据传递的类型切换,以推断要写入 JSON 的链接。(因此需要访问请求的主机、端口、方案等,以及类型本身以推断 URL 和已知字段等)

英文:

Given the following code: (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))
}
}

I'm attempting to have the rendered JSON look correct in my case that means something like:

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

Unfortunately Go is treating the anonymous member as a real named thing, so it's taking the defined type (Anything) and naming the JSON thusly:

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"
}
}

There's no clear docs on the handling of anonymous members in JSON, from the docs:

> Anonymous struct fields are usually marshaled as if their inner exported fields were fields in the outer struct, subject to the usual Go visibility rules amended as described in the next paragraph. An anonymous struct field with a name given in its JSON tag is treated as having that name, rather than being anonymous.
>
> Handling of anonymous struct fields is new in Go 1.1. Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of an anonymous struct field in both current and earlier versions, give the field a JSON tag of "-".

This doesn't make clear if there's a way to flatten out, or hint to the Marshaller what I am trying to do.

I'm certain that there might be, as there is a special case, magic name that has a special meaning to rename the root element of an XML document in the XML marshaller.

In this case, I'm also not attached to the code in any way, my use-case is to have a function that accepts interface{}, *http.Request, http.ResponseWriter and write back HATEOAS documents down the wire, switching on the type passed, to infer which links to write back into the JSON. (thus access to the request, for request host, port, scheme, etc, as well as to the type itself to infer the URL and known fields, etc)

答案1

得分: 3

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

其要点是:通过使用reflect包,我们循环遍历要序列化的结构体的字段,并将它们映射到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))
	}
}
英文:

Working playground link: http://play.golang.org/p/_r-bQIw347

The gist of it is this; by using the reflect package we loop over the fields of the struct we wish to serialize and map them to a map[string]interface{} we can now retain the flat structure of the original struct without introducing new fields.

Caveat emptor, there should probably be several checks against some of the assumptions made in this code. For instance it assumes that MarshalHateoas always receives pointers to values.

package main
import (
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;reflect&quot;
)
type User struct {
Id   int    `json:&quot;id&quot;`
Name string `json:&quot;name&quot;`
}
type Session struct {
Id     int `json:&quot;id&quot;`
UserId int `json:&quot;userId&quot;`
}
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 &lt; subjectType.NumField(); i++ {
field := subjectType.Field(i)
name := subjectType.Field(i).Name
out[field.Tag.Get(&quot;json&quot;)] = subjectValue.FieldByName(name).Interface()
}
switch s := subject.(type) {
case *User:
links[&quot;self&quot;] = fmt.Sprintf(&quot;http://user/%d&quot;, s.Id)
case *Session:
links[&quot;self&quot;] = fmt.Sprintf(&quot;http://session/%d&quot;, s.Id)
}
out[&quot;_links&quot;] = links
return json.MarshalIndent(out, &quot;&quot;, &quot;    &quot;)
}
func main() {
u := &amp;User{123, &quot;James Dean&quot;}
s := &amp;Session{456, 123}
json, err := MarshalHateoas(u)
if err != nil {
panic(err)
} else {
fmt.Println(&quot;User JSON:&quot;)
fmt.Println(string(json))
}
json, err = MarshalHateoas(s)
if err != nil {
panic(err)
} else {
fmt.Println(&quot;Session JSON:&quot;)
fmt.Println(string(json))
}
}

答案2

得分: 2

抱歉,但我认为你尝试生成的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 = jsonString1;

考虑给你的对象命名一个更好的考虑,例如:

{
"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;

希望这对你有帮助。

英文:

sorry, but I think the JSON you're trying to generate is not a valid JSON object and thus it may be the reason the JsonMarshal is not playing game with you.

The object may not consumable via JavaScript as it contains two objects, unless you wrap the objects in an array.

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

Then you would be able to consume this JSON, example:

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

Consider giving your objects root names might be a better consideration, example:

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

and consumed as, example:

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

I hope this helps you

答案3

得分: 0

利用omitempty标签和一些逻辑,您可以使用单个类型为不同情况生成正确的输出。

关键是要知道何时JSON编码器将值视为空值。根据encoding/json文档:

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

以下是稍作修改的程序,以生成您想要的输出。当值为"empty"时,它会省略特定字段-具体来说,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))
	}
}

在play.golang.org上复制代码。

英文:

Make use of omitempty tags and a bit of logic so you can use a single type that produces the right output for different cases.

The trick is knowing when a value is considered empty by the JSON encoder. From the encoding/json documentation:

> The empty values are false, 0, any nil pointer or interface value, and
> any array, slice, map, or string of length zero.

Here is your program slightly modified to produce the output you wanted. It omits certain fields when their values are "empty" - specifically, the JSON encoder will omit ints with "0" as value and maps with zero-length.

package main
import (
&quot;encoding/json&quot;
&quot;fmt&quot;
)
type User struct {
Id     int               `json:&quot;id&quot;`
Name   string            `json:&quot;name,omitempty&quot;`
UserId int               `json:&quot;userId,omitempty&quot;`
Links  map[string]string `json:&quot;_links,omitempty&quot;`
}
func Marshal(u *User) ([]byte, error) {
u.Links = make(map[string]string)
if u.UserId != 0 {
u.Links[&quot;self&quot;] = fmt.Sprintf(&quot;http://user/%d&quot;, u.UserId)
} else if u.Id != 0 {
u.Links[&quot;self&quot;] = fmt.Sprintf(&quot;http://session/%d&quot;, u.Id)
}
return json.MarshalIndent(u, &quot;&quot;, &quot;    &quot;)
}
func main() {
u := &amp;User{Id: 123, Name: &quot;James Dean&quot;}
s := &amp;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.

答案4

得分: 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),并且可能还需要使用reflect包,特别是因为任何内容都是interface{}类型。

英文:

One approach which I've worked through involves the use of methods on types. Please see http://play.golang.org/p/bPWB4ryDQn for more details.

Basically, you work the problem from the opposite angle -- instead of "encapsulating" a base type into a Hateoas type, you instead incorporate the required map in each of your base types. Then, implement a method on each of those base types, which is responsible for updating the Links field accordingly.

This produces the intended result, and with only marginal source code boiler-plate.

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

I believe any other way than this, particularly if you persue the embed-and-extend approach, will require implementing a custom marshaler (http://golang.org/pkg/encoding/json/#Marshaler) and will likely require the use of the reflect package as well, especially since anything is of type interface{}.

答案5

得分: 0

这有点棘手。但有一件确定的事情是,文档中提到了这样处理你的示例:

> 接口值编码为接口中包含的值

来自同一个链接,所以没有更多要做的了。"Anything"虽然是匿名的,但它是一个接口变量,所以我可能会期望在你的示例中看到这种行为。

我拿了你的代码并做了一些修改。这个示例可以工作,但有一些副作用。在这种情况下,我需要更改ID成员名称,以避免冲突。但我也改变了json标签。如果我不改变标签,那么代码似乎运行时间太长,重叠的标签都被省略了(这里)。

PS:我不能确定,但我猜测有一个假设存在问题。我会假设我要编组的任何东西都可以解组。但是你的扁平化程序做不到这一点。如果你想要一个扁平化程序,你可能需要分叉JSON编码器并向标签中添加一些值(就像有一个'omitempty'一样,你可以添加一个'flatten')。

英文:

This is kinda tricky. One thing for certain, however, is that the doc addresses your sample with this:

> Interface values encode as the value contained in the interface

from the same link so there is nothing more to do. "Anything" while it is anonymous it is an interface variable and so I might expect the behavior in your sample.

I took your code and made some changes. This example works but has some side effects. In this case I needed to change the ID member names so that there was nome collision. But then I also changed the json tag. If I did not change the tag then the code seemed to take too long to run and the overlapping tags were both omitted. (here).

PS: I cannot say with any certainty but I would guess that there is a problem with the assumption. I would assume that anything that I was going to Marshal I would want to be able to UnMarshal. Your flattener just won't do that. If you want a flattener you might have to fork the JSON encoder and add some values to the tags (much like there is a 'omitempty' you might add a 'flatten'.

答案6

得分: 0

我需要做类似的事情,并尝试了你的嵌入式接口技术(毫不奇怪)遇到了相同的问题。

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

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属性添加到原始结构体更好。但对于我的应用需求来说,这是最好的方法。

英文:

I needed to do something similar and tried your technical of the embedded interface and (no surprise) had the same issue.

I didn't want to have to alter all the possible structs that needed additional fields, but ultimately I compromised with this solution:

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

package main

import (
&quot;encoding/json&quot;
&quot;fmt&quot;
)
type HateoasFields struct {
Links []interface{} `json:&quot;_links,omitempty&quot;`
}
type User struct {
Id   int    `json:&quot;id&quot;`
Name string `json:&quot;name&quot;`
HateoasFields
}
type Session struct {
Id     int `json:&quot;id&quot;`
UserId int `json:&quot;userId&quot;`
HateoasFields
}
func main() {
u := &amp;User{Id: 123, Name: &quot;James Dean&quot;}
s := &amp;Session{Id: 456, UserId: 123}
u.Links = []interface{}{fmt.Sprintf(&quot;http://user/%d&quot;, u.Id)}
s.Links = []interface{}{fmt.Sprintf(&quot;http://session/%d&quot;, s.Id)}
uBytes, _ := json.Marshal(u)
sBytes, _ := json.Marshal(s)
fmt.Println(string(uBytes))
fmt.Println(string(sBytes))
}

Which outputs:

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

Rather than embedding the original structs in a struct with the additional fields, I did the opposite.

When looking at your original code, I don't think this solution is that awesome. How is this better than adding the Links property to the original struct? But for my app needs it was the best thing.

答案7

得分: 0

我喜欢这个解决方案,如果你只需要担心地图的话。

// Flatten函数接受一个地图,并返回一个新的地图,其中嵌套的地图被替换为点分隔的键。
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
}

英文:

I like this solution if you only have maps to worry about.

// 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+&quot;.&quot;+nk] = nv
}
default:
o[k] = v
}
}
return o
}

huangapple
  • 本文由 发表于 2013年12月4日 05:34:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/20362147.html
匿名

发表评论

匿名网友

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

确定