如何在 Go 中为通用类型展平 JSON

我正在尝试在 Go 中实现 HAL,只是想看看我是否可以。这意味着我有一个对HAL有效负载通用的类型,并且还包含_links:


type HAL[T any] struct {

    Payload T

    Links   Linkset `json:"_links,omitempty"`

}

在 HAL 规范中,有效负载实际上位于顶层,而不是嵌套在其中 - 就像 Siren 一样。所以这意味着给出以下内容:


type TestPayload struct {

    Name   string `json:"name"`

    Answer int    `json:"answer"`

}


    hal := HAL[TestPayload]{

        Payload: TestPayload{

            Name:   "Graham",

            Answer: 42,

        },

        Links: Linkset{

            "self": {

                {Href: "/"},

            },

        },

    }

生成的 JSON 应该是:


{

    "name": "Graham",

    "answer": 42,

    "_links": {

      "self": {"href": "/"}

    }

}

但是我想不出一个好的方法来让这个 JSON 编组工作。


我已经看到将有效负载嵌入为匿名成员的建议,如果它不是通用的,它会很好用。不幸的是,您不能以这种方式嵌入泛型类型,所以这是行不通的。


我可能可以编写一个MarshalJSON方法来完成这项工作,但我想知道是否有任何标准方法来实现这一点?


白衣染霜花
浏览 133回答 4
4回答

慕的地10843

是的,不幸的是你不能嵌入类型参数T。我还将争辩说,在一般情况下,您不应该尝试展平输出 JSON。通过约束Twith&nbsp;any,您几乎可以接受任何类型,但并非所有类型都有可以提升到您的HAL结构中的字段。这在语义上是不一致的。如果您尝试嵌入没有字段的类型,输出的 JSON 将不同。以解决方案为例reflect.StructOf,没有什么能阻止我实例化HAL[[]int]{ Payload: []int{1,2,3}, Links: ... },在这种情况下,输出将是:{"X":[1,2,3],"Links":{"self":{"href":"/"}}}这会使您的 JSON 序列化随用于实例化的类型发生变化T,这对于阅读您的代码的人来说不容易发现。代码的可预测性较低,并且您正在有效地对抗类型参数提供的泛化。使用命名字段Payload T更好,因为:输出 JSON 始终(对于大多数意图和目的)与实际结构一致解组也保持可预测的行为代码的可伸缩性不是问题,因为您不必重复HAL构建匿名结构的所有字段OTOH,如果您的要求恰好是将结构编组为扁平化,而其他所有内容都带有键(HAL 类型可能就是这种情况),至少通过检查实现使其显而易见,并为任何情况提供reflect.ValueOf(hal.Payload).Kind() == reflect.Struct默认MarshalJSON情况否则T可能。将不得不在JSONUnmarshal.T这是一个带有反射的解决方案,当您将更多字段添加到主结构时,它可以在不是结构时工作并缩放:// necessary to marshal HAL without causing infinite loop// can't declare inside the method due to a current limitation with Go genericstype tmp[T any] HAL[T]func (h HAL[T]) MarshalJSON() ([]byte, error) {&nbsp; &nbsp; // examine Payload, if it isn't a struct, i.e. no embeddable fields, marshal normally&nbsp; &nbsp; v := reflect.ValueOf(h.Payload)&nbsp; &nbsp; if v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {&nbsp; &nbsp; &nbsp; &nbsp; v = v.Elem()&nbsp; &nbsp; }&nbsp; &nbsp; if v.Kind() != reflect.Struct {&nbsp; &nbsp; &nbsp; &nbsp; return json.Marshal(tmp[T](h))&nbsp; &nbsp; }&nbsp; &nbsp; // flatten all fields into a map&nbsp; &nbsp; m := make(map[string]any)&nbsp; &nbsp; // flatten Payload first&nbsp; &nbsp; for i := 0; i < v.NumField(); i++ {&nbsp; &nbsp; &nbsp; &nbsp; key := jsonkey(v.Type().Field(i))&nbsp; &nbsp; &nbsp; &nbsp; m[key] = v.Field(i).Interface()&nbsp; &nbsp; }&nbsp; &nbsp; // flatten the other fields&nbsp; &nbsp; w := reflect.ValueOf(h)&nbsp; &nbsp; // start at 1 to skip the Payload field&nbsp; &nbsp; for i := 1; i < w.NumField(); i++ {&nbsp; &nbsp; &nbsp; &nbsp; key := jsonkey(w.Type().Field(i))&nbsp; &nbsp; &nbsp; &nbsp; m[key] = w.Field(i).Interface()&nbsp; &nbsp; }&nbsp; &nbsp; return json.Marshal(m)}func jsonkey(field reflect.StructField) string {&nbsp; &nbsp; // trickery to get the json tag without omitempty and whatnot&nbsp; &nbsp; tag := field.Tag.Get("json")&nbsp; &nbsp; tag, _, _ = strings.Cut(tag, ",")&nbsp; &nbsp; if tag == "" {&nbsp; &nbsp; &nbsp; &nbsp; tag = field.Name&nbsp; &nbsp; }&nbsp; &nbsp; return tag}用HAL[TestPayload]orHAL[*TestPayload]它输出:{"answer":42,"name":"Graham","_links":{"self":{"href":"/"}}}用HAL[[]int]它输出:{"Payload":[1,2,3],"_links":{"self":{"href":"/"}}}游乐场:https://go.dev/play/p/bWGXWj_rC5F

尚方宝剑之说

我会制作一个自定义 JSON 编解码器,_links在为有效负载生成的 JSON 末尾插入字段。编组器。type Link struct {&nbsp; &nbsp; Href string `json:"href"`}type Linkset map[string]Linktype HAL[T any] struct {&nbsp; &nbsp; Payload T&nbsp; &nbsp; Links&nbsp; &nbsp;Linkset `json:"_links,omitempty"`}func (h HAL[T]) MarshalJSON() ([]byte, error) {&nbsp; &nbsp; payloadJson, err := json.Marshal(h.Payload)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return nil, err&nbsp; &nbsp; }&nbsp; &nbsp; if len(payloadJson) == 0 {&nbsp; &nbsp; &nbsp; &nbsp; return nil, fmt.Errorf("Empty payload")&nbsp; &nbsp; }&nbsp; &nbsp; if h.Links != nil {&nbsp; &nbsp; &nbsp; &nbsp; return appendField(payloadJson, "_links", h.Links)&nbsp; &nbsp; }&nbsp; &nbsp; return payloadJson, nil}func appendField[T any](raw []byte, fieldName string, v T) ([]byte, error) {&nbsp; &nbsp; // The JSON data must be braced in {}&nbsp; &nbsp; if raw[0] != '{' || raw[len(raw)-1] != '}' {&nbsp; &nbsp; &nbsp; &nbsp; return nil, fmt.Errorf("Not an object: %s", string(raw))&nbsp; &nbsp; }&nbsp; &nbsp; valJson, err := json.Marshal(v)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return nil, err&nbsp; &nbsp; }&nbsp; &nbsp; // Add the field at the end of the json text&nbsp; &nbsp; result := bytes.NewBuffer(raw[:len(raw)-1])&nbsp; &nbsp; // Append `"<fieldName>":value`&nbsp; &nbsp; // Insert comma if the `raw` object is not empty&nbsp; &nbsp; if len(raw) > 2 {&nbsp; &nbsp; &nbsp; &nbsp; result.WriteByte(',')&nbsp; &nbsp; }&nbsp; &nbsp; // tag&nbsp; &nbsp; result.WriteByte('"')&nbsp; &nbsp; result.WriteString(fieldName)&nbsp; &nbsp; result.WriteByte('"')&nbsp; &nbsp; // colon&nbsp; &nbsp; result.WriteByte(':')&nbsp; &nbsp; // value&nbsp; &nbsp; result.Write(valJson)&nbsp; &nbsp; // closing brace&nbsp; &nbsp; result.WriteByte('}')&nbsp; &nbsp; return result.Bytes(), nil}Payload如果序列化为 JSON 对象以外的对象,编组器将返回错误。原因是编解码器_links只能为对象添加字段。解组器:func (h *HAL[T]) UnmarshalJSON(raw []byte) error {&nbsp; &nbsp; // Unmarshal fields of the payload first.&nbsp; &nbsp; // Unmarshal the whole JSON into the payload, it is safe:&nbsp; &nbsp; // decorer ignores unknow fields and skips "_links".&nbsp; &nbsp; if err := json.Unmarshal(raw, &h.Payload); err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return err&nbsp; &nbsp; }&nbsp; &nbsp; // Get "_links": scan trough JSON until "_links" field&nbsp; &nbsp; links := make(Linkset)&nbsp; &nbsp; exists, err := extractField(raw, "_links", &links)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return err&nbsp; &nbsp; }&nbsp; &nbsp; if exists {&nbsp; &nbsp; &nbsp; &nbsp; h.Links = links&nbsp; &nbsp; }&nbsp; &nbsp; return nil}func extractField[T any](raw []byte, fieldName string, v *T) (bool, error) {&nbsp; &nbsp; // Scan through JSON until field is found&nbsp; &nbsp; decoder := json.NewDecoder(bytes.NewReader(raw))&nbsp; &nbsp; t := must(decoder.Token())&nbsp; &nbsp; // should be `{`&nbsp; &nbsp; if t != json.Delim('{') {&nbsp; &nbsp; &nbsp; &nbsp; return false, fmt.Errorf("Not an object: %s", string(raw))&nbsp; &nbsp; }&nbsp; &nbsp; t = must(decoder.Token())&nbsp; &nbsp; if t == json.Delim('}') {&nbsp; &nbsp; &nbsp; &nbsp; // Empty object&nbsp; &nbsp; &nbsp; &nbsp; return false, nil&nbsp; &nbsp; }&nbsp; &nbsp; for decoder.More() {&nbsp; &nbsp; &nbsp; &nbsp; name, ok := t.(string)&nbsp; &nbsp; &nbsp; &nbsp; if !ok {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return false, fmt.Errorf("must never happen: expected string, got `%v`", t)&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if name != fieldName {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; skipValue(decoder)&nbsp; &nbsp; &nbsp; &nbsp; } else {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if err := decoder.Decode(v); err != nil {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return false, err&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return true, nil&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if decoder.More() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t = must(decoder.Token())&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; return false, nil}func skipValue(d *json.Decoder) {&nbsp; &nbsp; braceCnt := 0&nbsp; &nbsp; for d.More() {&nbsp; &nbsp; &nbsp; &nbsp; t := must(d.Token())&nbsp; &nbsp; &nbsp; &nbsp; if t == json.Delim('{') || t == json.Delim('[') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; braceCnt++&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if t == json.Delim('}') || t == json.Delim(']') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; braceCnt--&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if braceCnt == 0 {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}解组器在非对象上也会失败。需要读取_links字段。为此,输入必须是一个对象。完整示例:https://go.dev/play/p/E3NN2T7Fbnmfunc main() {&nbsp; &nbsp; hal := HAL[TestPayload]{&nbsp; &nbsp; &nbsp; &nbsp; Payload: TestPayload{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Name:&nbsp; &nbsp;"Graham",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Answer: 42,&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp; &nbsp; &nbsp; &nbsp; Links: Linkset{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "self": Link{Href: "/"},&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp; &nbsp; }&nbsp; &nbsp; bz := must(json.Marshal(hal))&nbsp; &nbsp; println(string(bz))&nbsp; &nbsp; var halOut HAL[TestPayload]&nbsp; &nbsp; err := json.Unmarshal(bz, &halOut)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; println("Decode failed: ", err.Error())&nbsp; &nbsp; }&nbsp; &nbsp; fmt.Printf("%#v\n", halOut)}输出:{"name":"Graham","answer":42,"_links":{"self":{"href":"/"}}}main.HAL[main.TestPayload]{Payload:main.TestPayload{Name:"Graham", Answer:42}, Links:main.Linkset{"self":main.Link{Href:"/"}}}

蝴蝶不菲

把事情简单化。是的,嵌入类型会很好 - 但由于目前不可能(从 开始go1.19)嵌入泛型类型 - 只需将其写成内联:body, _ = json.Marshal(&nbsp; &nbsp; struct {&nbsp; &nbsp; &nbsp; &nbsp; TestPayload&nbsp; &nbsp; &nbsp; &nbsp; Links&nbsp; &nbsp; &nbsp; &nbsp;Linkset `json:"_links,omitempty"`&nbsp; &nbsp; }{&nbsp; &nbsp; &nbsp; &nbsp; TestPayload: hal.Payload,&nbsp; &nbsp; &nbsp; &nbsp; Links:&nbsp; &nbsp; &nbsp; &nbsp;hal.Links,&nbsp; &nbsp; },)https://go.dev/play/p/8yrB-MzUVK-{&nbsp; &nbsp; "name": "Graham",&nbsp; &nbsp; "answer": 42,&nbsp; &nbsp; "_links": {&nbsp; &nbsp; &nbsp; &nbsp; "self": {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "href": "/"&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}是的,约束类型需要被引用两次——但所有自定义都是代码本地化的,因此不需要自定义封送拆收器。

牧羊人nacy

是的,嵌入是最简单的方法,正如您所写,您目前无法嵌入类型参数。但是,您可以构造一个使用反射嵌入类型参数的类型。我们可以实例化此类型并对其进行编组。例如:func (hal HAL[T]) MarshalJSON() ([]byte, error) {&nbsp; &nbsp; t := reflect.StructOf([]reflect.StructField{&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Name:&nbsp; &nbsp; &nbsp; "X",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Anonymous: true,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Type:&nbsp; &nbsp; &nbsp; reflect.TypeOf(hal.Payload),&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Name: "Links",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Type: reflect.TypeOf(hal.Links),&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp; &nbsp; })&nbsp; &nbsp; v := reflect.New(t).Elem()&nbsp; &nbsp; v.Field(0).Set(reflect.ValueOf(hal.Payload))&nbsp; &nbsp; v.Field(1).Set(reflect.ValueOf(hal.Links))&nbsp; &nbsp; return json.Marshal(v.Interface())}这将输出(在Go Playground上尝试):{"name":"Graham","answer":42,"Links":{"self":{"href":"/"}}}
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go