Generic type definition for unmarshalling structs into slices

王林
Release: 2024-02-06 08:54:04
forward
1081 people have browsed it

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

results in the following error:

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

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
}
Copy after login

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!

source:stackoverflow.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!