Я пытаюсь реализовать HAL в Go, просто чтобы посмотреть, смогу ли я. Это означает, что у меня есть тип HAL, который является общим для полезной нагрузки, а также содержит _links:
type HAL[T any] struct {
Payload T
Links Linkset `json:"_links,omitempty"`
}
В спецификации HAL полезная нагрузка фактически находится на верхнем уровне, а не вложена в нее - например, например. Сирена будет. Итак, это означает следующее:
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, который выполнит эту работу, но мне интересно, есть ли вместо этого какой-либо стандартный способ добиться этого?
У меня есть ссылка Playground с этим рабочим кодом, чтобы посмотреть, поможет ли он: https://go.dev/play/p/lorK5Wv-Tri
Ваше здоровье

Да, встраивание - это самый простой способ, и, как вы написали, в настоящее время вы не можете встроить параметр типа.
Однако вы можете создать тип, который включает параметр типа, используя отражение. Вместо этого мы можем создать экземпляр этого типа и маршалировать его.
Например:
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":"/"}}}
См. также: Добавление произвольных полей в вывод json неизвестной структуры
Будь проще.
Да, было бы неплохо встроить тип, но поскольку в настоящее время невозможно (начиная с 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": "/"
}
}
}
Да, на тип ограничения нужно ссылаться дважды, но все настройки локализованы в коде, поэтому нет необходимости в специальном упаковщике.
Ах! Нужно в yaml - полезно знать.
путем явного встраивания TestPayload сортировка больше не является общей, а HAL[T] в OP является общей
Чтобы использовать универсальные типы, необходимо создать экземпляр типа, т.е. HAL[TestPayload]. Вышеупомянутое более многословно, да - повторяет определение типа - но по сути то же самое: предоставление конкретного типа во время компиляции. Учитывая текущие ограничения встраивания, это самое близкое, что может получить OP.
Да, к сожалению, нельзя встроить параметр типа T. Я также утверждаю, что в общем случае вам не следует пытаться сглаживать выходные данные JSON. Ограничивая T с помощью any, вы допускаете буквально любой тип, однако не у всех типов есть поля для включения в вашу структуру HAL.
Это семантически несовместимо.
Если вы попытаетесь внедрить тип без полей, выходной JSON будет другим. Используя решение с reflect.StructOf в качестве примера, ничто не мешает мне создать экземпляр HAL[[]int]{ Payload: []int{1,2,3}, Links: ... }, и в этом случае вывод будет таким:
{"X":[1,2,3],"Links":{"self":{"href":"/"}}}
Это изменяет вашу сериализацию JSON с типами, используемыми для создания экземпляров T, что нелегко заметить тому, кто читает ваш код. Код менее предсказуем, и вы эффективно работаете против обобщения, которое обеспечивают параметры типа.
Лучше использовать именованное поле Payload T, так как:
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] или HAL[*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 {
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 только к объектам.
Unmarshaller:
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
}
}
}
Unmarshaller также терпит неудачу на не-объекте. Необходимо прочитать поле _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:"/"}}}
это приемлемое решение в теории, если сложность маршала/демаршала оправдана для использования, однако это трудно сделать правильно. На самом деле ваш код паникует, если HAL создается с помощью чего-то другого, кроме структуры.
@blackgreen уверен, что это не удается. Невозможно добавить поле _links к чему-либо, кроме объекта. И нет возможности извлечь _links из не-объектов. Что вы понимаете под сложностью? Этот кодек намного проще по сравнению с json.Decoder
Опция
,inlineнеизвестна библиотекеencoding/json. Что заставляет эту работу работать, так это просто факт встраивания.