Home  >  Article  >  Backend Development  >  Generic type definition for unmarshalling structs into slices

Generic type definition for unmarshalling structs into slices

王林
王林forward
2024-02-06 08:54:041105browse

Generic type definition for unmarshalling structs into slices

Question content

I have an API that normally returns an array as an object containing the array. Take the following example as an example:

<code>{
  "items": {
    "number": 3,
    "item": [
      { ... } // Not relevant
    ]
  }
}
</code>

The API does this in dozens of places, each time with a different name. This is guaranteed to happen with only two keys: one of which is number and the other of which is an array.

This makes the resulting structure rather unpleasant to use, since you have to constantly browse through unnecessary levels of fields.

I essentially want my Go interface to pretend it has this format:

<code>{
  "items": [
    { ... } // Not relevant
  ]
}
</code>

One option is to write a custom UnmarshalJSON function for each occurrence, but this seems cumbersome, especially considering that it appears in almost every structure. The solution I came up with is a generic type that can handle it on its own.

My current attempt is as follows:

<code>// NestedArray tries to pull an unnecessarily nested array upwards
type NestedArray[T any] []T

func (n *NestedArray[T]) UnmarshalJSON(bytes []byte) error {
    // First unmarshal into a map
    target := make(map[string]interface{})

    err := json.Unmarshal(bytes, &target)
    if err != nil {
        return err
    }

    // Then find the nested array (key is unknown, so go off of the type instead)
    var sliceVal interface{}
    for k, v := range target {
        if k == "number" {
            continue
        }

        rt := reflect.TypeOf(v)
        if rt.Kind() == reflect.Slice {
            sliceVal = v
            break
        }
    }

    // Missing or empty, doesn't matter - set the result to nil
    if sliceVal == nil {
        *n = nil
        return nil
    }

    // Turn back into JSON and parse into correct target
    sliceJSON, err := json.Marshal(sliceVal)
    if err != nil {
        return err
    }

    err = json.Unmarshal(sliceJSON, n)  // Error occurs here
    if err != nil {
        return err
    }

    return nil
}
</code>

Usage is as follows:

<code>type Item struct {
  // Not relevant
}

type Root struct {
    // Use generic type to parse a JSON object into its nested array
    Items NestedArray[Item] `json:"items,omitempty"`
}
</code>

results in the following error:

json: cannot unmarshal array into Go struct field Root.items of type map[string]interface{}

UnmarshalJSON The largest part of the code seems to be correct, as my debugger shows me sliceVal exactly what I expect. Error unmarshalling back to type NestedArray[T].

Is there any way to solve this problem? Is there a better way than what I'm doing now? This seems the cleanest to me, but I'm open to suggestions.


Correct Answer


Method NestedArray[T].UnmarshalJSON calls itself recursively. The inner call throws an error because it expects a JSON object in bytes, but it receives a JSON array. Fixed by unmarshalling to []T instead of NestedArray[T].

Unrelated to the error, the method NestedArray[T].UnmarshalJSON performed some unnecessary encoding and decoding. Use json.RawMessage to fix it.

Here is the code with two fixes:

func (n *NestedArray[T]) UnmarshalJSON(bytes []byte) error {
    // First unmarshal into a map
    var target map[string]json.RawMessage
    err := json.Unmarshal(bytes, &target)
    if err != nil {
        return err
    }

    // Then find the nested array (key is unknown, so go off of the type instead)
    var array json.RawMessage
    for k, v := range target {
        if k == "number" {
            continue
        }
        if len(v) > 0 && v[0] == '[' {
            array = v
            break
        }
    }

    // Missing or empty, doesn't matter - set the result to nil
    if array == nil {
        *n = nil
        return nil
    }

    // Avoid recursive call to this method by unmarshalling to a []T.
    var v []T
    err = json.Unmarshal(array, &v)
    *n = v
    return err
}

Run code on the Playground! .

The above is the detailed content of Generic type definition for unmarshalling structs into slices. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:stackoverflow.com. If there is any infringement, please contact admin@php.cn delete