Как сгладить JSON для универсального типа в Go

Я пытаюсь реализовать 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

Ваше здоровье

Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
4
0
161
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Да, встраивание - это самый простой способ, и, как вы написали, в настоящее время вы не можете встроить параметр типа.

Однако вы можете создать тип, который включает параметр типа, используя отражение. Вместо этого мы можем создать экземпляр этого типа и маршалировать его.

Например:

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

Да, на тип ограничения нужно ссылаться дважды, но все настройки локализованы в коде, поэтому нет необходимости в специальном упаковщике.

Опция ,inline неизвестна библиотеке encoding/json. Что заставляет эту работу работать, так это просто факт встраивания.

icza 15.10.2022 19:05

Ах! Нужно в yaml - полезно знать.

colm.anseo 15.10.2022 19:16

путем явного встраивания TestPayload сортировка больше не является общей, а HAL[T] в OP является общей

blackgreen 16.10.2022 10:00

Чтобы использовать универсальные типы, необходимо создать экземпляр типа, т.е. HAL[TestPayload]. Вышеупомянутое более многословно, да - повторяет определение типа - но по сути то же самое: предоставление конкретного типа во время компиляции. Учитывая текущие ограничения встраивания, это самое близкое, что может получить OP.

colm.anseo 16.10.2022 14:39
Ответ принят как подходящий

Да, к сожалению, нельзя встроить параметр типа 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, так как:

  • выходной JSON всегда (для большинства намерений и целей) согласуется с фактической структурой
  • unmarshalling также сохраняет предсказуемое поведение
  • масштабируемость кода не является проблемой, так как вам не нужно повторять все поля 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 16.10.2022 09:45

@blackgreen уверен, что это не удается. Невозможно добавить поле _links к чему-либо, кроме объекта. И нет возможности извлечь _links из не-объектов. Что вы понимаете под сложностью? Этот кодек намного проще по сравнению с json.Decoder

Pak Uula 16.10.2022 10:48

Другие вопросы по теме