Home > Article > Backend Development > Generic type definition for unmarshalling structs into slices
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.
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 }
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!