为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

Go
白衣染霜花 2023-02-28 21:17:12
我正在尝试在 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方法来完成这项工作,但我想知道是否有任何标准方法来实现这一点?
查看完整描述

4 回答

?
慕的地10843

TA贡献1785条经验 获得超8个赞

是的,不幸的是你不能嵌入类型参数T。我还将争辩说,在一般情况下,您不应该尝试展平输出 JSON。通过约束Twith 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 generics

type tmp[T any] HAL[T]


func (h HAL[T]) MarshalJSON() ([]byte, error) {

    // examine Payload, if it isn't a struct, i.e. no embeddable fields, marshal normally

    v := reflect.ValueOf(h.Payload)

    if v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {

        v = v.Elem()

    }

    if v.Kind() != reflect.Struct {

        return json.Marshal(tmp[T](h))

    }


    // flatten all fields into a map

    m := make(map[string]any)

    // flatten Payload first

    for i := 0; i < v.NumField(); i++ {

        key := jsonkey(v.Type().Field(i))

        m[key] = v.Field(i).Interface()

    }

    // flatten the other fields

    w := reflect.ValueOf(h)

    // start at 1 to skip the Payload field

    for i := 1; i < w.NumField(); i++ {

        key := jsonkey(w.Type().Field(i))

        m[key] = w.Field(i).Interface()

    }

    return json.Marshal(m)

}


func jsonkey(field reflect.StructField) string {

    // trickery to get the json tag without omitempty and whatnot

    tag := field.Tag.Get("json")

    tag, _, _ = strings.Cut(tag, ",")

    if tag == "" {

        tag = field.Name

    }

    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


查看完整回答
反对 回复 2023-02-28
?
尚方宝剑之说

TA贡献1788条经验 获得超4个赞

我会制作一个自定义 JSON 编解码器,_links在为有效负载生成的 JSON 末尾插入字段。


编组器。



type Link struct {

    Href string `json:"href"`

}


type Linkset map[string]Link


type HAL[T any] struct {

    Payload T

    Links   Linkset `json:"_links,omitempty"`

}


func (h HAL[T]) MarshalJSON() ([]byte, error) {

    payloadJson, err := json.Marshal(h.Payload)

    if err != nil {

        return nil, err

    }

    if len(payloadJson) == 0 {

        return nil, fmt.Errorf("Empty payload")

    }

    if h.Links != nil {

        return appendField(payloadJson, "_links", h.Links)

    }

    return payloadJson, nil

}


func appendField[T any](raw []byte, fieldName string, v T) ([]byte, error) {

    // The JSON data must be braced in {}

    if raw[0] != '{' || raw[len(raw)-1] != '}' {

        return nil, fmt.Errorf("Not an object: %s", string(raw))

    }

    valJson, err := json.Marshal(v)

    if err != nil {

        return nil, err

    }

    // Add the field at the end of the json text

    result := bytes.NewBuffer(raw[:len(raw)-1])

    // Append `"<fieldName>":value`

    // Insert comma if the `raw` object is not empty

    if len(raw) > 2 {

        result.WriteByte(',')

    }

    // tag

    result.WriteByte('"')

    result.WriteString(fieldName)

    result.WriteByte('"')

    // colon

    result.WriteByte(':')

    // value

    result.Write(valJson)

    // closing brace

    result.WriteByte('}')

    return result.Bytes(), nil

}

Payload如果序列化为 JSON 对象以外的对象,编组器将返回错误。原因是编解码器_links只能为对象添加字段。


解组器:


func (h *HAL[T]) UnmarshalJSON(raw []byte) error {

    // Unmarshal fields of the payload first.

    // Unmarshal the whole JSON into the payload, it is safe:

    // decorer ignores unknow fields and skips "_links".

    if err := json.Unmarshal(raw, &h.Payload); err != nil {

        return err

    }

    // Get "_links": scan trough JSON until "_links" field

    links := make(Linkset)

    exists, err := extractField(raw, "_links", &links)

    if err != nil {

        return err

    }

    if exists {

        h.Links = links

    }

    return nil

}


func extractField[T any](raw []byte, fieldName string, v *T) (bool, error) {

    // Scan through JSON until field is found

    decoder := json.NewDecoder(bytes.NewReader(raw))

    t := must(decoder.Token())

    // should be `{`

    if t != json.Delim('{') {

        return false, fmt.Errorf("Not an object: %s", string(raw))

    }

    t = must(decoder.Token())

    if t == json.Delim('}') {

        // Empty object

        return false, nil

    }

    for decoder.More() {

        name, ok := t.(string)

        if !ok {

            return false, fmt.Errorf("must never happen: expected string, got `%v`", t)

        }

        if name != fieldName {

            skipValue(decoder)

        } else {

            if err := decoder.Decode(v); err != nil {

                return false, err

            }

            return true, nil

        }

        if decoder.More() {

            t = must(decoder.Token())

        }

    }

    return false, nil

}


func skipValue(d *json.Decoder) {

    braceCnt := 0

    for d.More() {

        t := must(d.Token())

        if t == json.Delim('{') || t == json.Delim('[') {

            braceCnt++

        }

        if t == json.Delim('}') || t == json.Delim(']') {

            braceCnt--

        }

        if braceCnt == 0 {

            return

        }

    }

}

解组器在非对象上也会失败。需要读取_links字段。为此,输入必须是一个对象。


完整示例:https://go.dev/play/p/E3NN2T7Fbnm


func main() {

    hal := HAL[TestPayload]{

        Payload: TestPayload{

            Name:   "Graham",

            Answer: 42,

        },

        Links: Linkset{

            "self": Link{Href: "/"},

        },

    }

    bz := must(json.Marshal(hal))

    println(string(bz))


    var halOut HAL[TestPayload]

    err := json.Unmarshal(bz, &halOut)

    if err != nil {

        println("Decode failed: ", err.Error())

    }

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



查看完整回答
反对 回复 2023-02-28
?
蝴蝶不菲

TA贡献1810条经验 获得超4个赞

把事情简单化。


是的,嵌入类型会很好 - 但由于目前不可能(从 开始go1.19)嵌入泛型类型 - 只需将其写成内联:


body, _ = json.Marshal(

    struct {

        TestPayload

        Links       Linkset `json:"_links,omitempty"`

    }{

        TestPayload: hal.Payload,

        Links:       hal.Links,

    },

)

https://go.dev/play/p/8yrB-MzUVK-


{

    "name": "Graham",

    "answer": 42,

    "_links": {

        "self": {

            "href": "/"

        }

    }

}

是的,约束类型需要被引用两次——但所有自定义都是代码本地化的,因此不需要自定义封送拆收器。


查看完整回答
反对 回复 2023-02-28
?
牧羊人nacy

TA贡献1862条经验 获得超7个赞

是的,嵌入是最简单的方法,正如您所写,您目前无法嵌入类型参数。


但是,您可以构造一个使用反射嵌入类型参数的类型。我们可以实例化此类型并对其进行编组。


例如:


func (hal HAL[T]) MarshalJSON() ([]byte, error) {

    t := reflect.StructOf([]reflect.StructField{

        {

            Name:      "X",

            Anonymous: true,

            Type:      reflect.TypeOf(hal.Payload),

        },

        {

            Name: "Links",

            Type: reflect.TypeOf(hal.Links),

        },

    })


    v := reflect.New(t).Elem()

    v.Field(0).Set(reflect.ValueOf(hal.Payload))

    v.Field(1).Set(reflect.ValueOf(hal.Links))


    return json.Marshal(v.Interface())

}

这将输出(在Go Playground上尝试):


{"name":"Graham","answer":42,"Links":{"self":{"href":"/"}}}


查看完整回答
反对 回复 2023-02-28
  • 4 回答
  • 0 关注
  • 132 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信