Home > Mobile >  How to flatten JSON for a generic type in Go
How to flatten JSON for a generic type in Go

Time:10-16

I'm trying to implement HAL in Go, just to see if I can. This means that I've got a HAL type that is generic over the payload, and also contains the _links:

type HAL[T any] struct {
    Payload T
    Links   Linkset `json:"_links,omitempty"`
}

In the HAL spec, the payload is actually at the top level and not nested inside it - like, e.g. Siren would be. So that means given the following:

type TestPayload struct {
    Name   string `json:"name"`
    Answer int    `json:"answer"`
}

    hal := HAL[TestPayload]{
        Payload: TestPayload{
            Name:   "Graham",
            Answer: 42,
        },
        Links: Linkset{
            "self": {
                {Href: "/"},
            },
        },
    }

The resulting JSON should be:

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

But I can't work out a good way to get this JSON marshalling to work.

I've seen suggestions of embedding the payload as an anonymous member, which works great if it's not generic. Unfortunately, you can't embed generic types in that way so that's a non-starter.

I probably could write a MarshalJSON method that will do the job, but I'm wondering if there's any standard way to achieve this instead?

I've got a Playground link with this working code to see if it helps: https://go.dev/play/p/lorK5Wv-Tri

Cheers

CodePudding user response:

Keep it simple.

Yes it would be nice to embed the type - but since it's not currently possible (as of go1.19) to embed a generic type - just write it out inline:

body, _ = json.Marshal(
    struct {
        TestPayload `json:",inline"`  // inline promotes the JSON tags
        Links       Linkset `json:"_links,omitempty"`
    }{
        TestPayload: hal.Payload,
        Links:       hal.Links,
    },
)

https://go.dev/play/p/vbTbQVYyJ39

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

Yes, the constraint type needs to be referenced twice - but all the customization is code-localized, so no need for a custom marshaler.

CodePudding user response:

You can't embed the type parameter T and you shouldn't attempt to flatten the output JSON. By constraining T with any, you are admitting literally any type, however not all types have fields to promote into your HAL struct.

This is semantically inconsistent.

This becomes apparent if you attempt to embed a type with no fields, giving a different output JSON. Using the solution with reflect.StructOf as an example, nothing stops me from instantiating HAL[[]int]{ Payload: []int{1,2,3}, Links: ... }, in which case the output would be:

{"X":42,"Links":{"self":{"href":"/"}}}

This makes your JSON serialization change with the types used to instantiate T, which is not easy to spot for someone who reads your code. You're making your code more obscure and less predictable, and effectively working against the generalization that type parameters provide.

Using the named field Payload T is just better, as then the output JSON is always (for most intents and purposes) consistent with the actual structure of HAL.

OTOH, if your requirements are precisely to marshal structs as flattened and everything else with a key, at the very least make it obvious by checking reflect.TypeOf(hal.Payload).Kind() === reflect.Struct in the MarshalJSON implementation, and provide a default case for whatever else T could be.

CodePudding user response:

Yes, embedding is the easiest way, and as you wrote, you can't currently embed a type parameter.

You may however construct a type that embeds the type param using reflection. We may instantiate this type and marshal it instead.

For example:

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())
}

This will output (try it on the Go Playground):

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

See related: Adding Arbitrary fields to json output of an unknown struct

  • Related