I have an API that often returns arrays as an object that contains an array. Take the example below:
我有一个API,它经常将数组作为包含数组的对象返回。下面是一个例子:
{
"items": {
"number": 3,
"item": [
{ ... } // Not relevant
]
}
}
The API does this in dozens of places with a different name each time. It is guaranteed that when this occurs there are only two keys: one of them being number
and the other being the array.
API在数十个地方执行此操作,每次都使用不同的名称。可以保证,当发生这种情况时,只有两个键:一个是数字,另一个是数组。
This makes the resulting structs rather unpleasant to work with, as you constantly have to navigate through levels of unnecessary fields.
这使得生成的结构非常难于使用,因为您必须不断地在不必要的字段级别中导航。
I essentially want my Go interface to pretend it had this format instead:
我实际上想让我的Go界面假装它的格式是这样的:
{
"items": [
{ ... } // Not relevant
]
}
One option is to write a custom UnmarshalJSON
function for every single occurrence, but this seems cumbersome, especially considering this appears in nearly every struct. The solution I had in mind is a generic type that can handle it on its own.
一种选择是为每个事件编写一个定制的UnmarshalJSON函数,但这似乎很麻烦,特别是考虑到这几乎出现在每个结构中。我心目中的解决方案是一种可以自己处理的泛型类型。
My current attempt is below:
我目前的尝试如下:
// 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
}
Using it as follows:
按如下方式使用它:
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"`
}
Results in the following error:
导致以下错误:
json: cannot unmarshal array into Go struct field Root.items of type map[string]interface{}
The biggest part of UnmarshalJSON
code seems correct, as my debugger shows me that sliceVal
is what I'd expect it to be. It errors when unmarshalling back into the NestedArray[T]
type.
UnmarshalJSON代码的最大部分似乎是正确的,因为我的调试器向我展示了sliceVal是我所期望的。将数据解组回NestedArray[T]类型时出错。
What could be the solution to this problem? Is there a better way to go about it than what I'm currently doing? This seemed the cleanest to me but I'm open to suggestions.
这个问题的解决方案是什么?有没有比我现在所做的更好的办法呢?这对我来说似乎是最干净的,但我愿意接受建议。
更多回答
优秀答案推荐
The method NestedArray[T].UnmarshalJSON calls itself recursively. The inner call throws an error because it's expecting a JSON object in bytes
, but it received a JSON array. Fix by unmarshalling to a []T
instead of a NestedArray[T]
.
方法NestedArray[T].UnmarshalJSON递归地调用自身。内部调用抛出一个错误,因为它需要一个以字节为单位的JSON对象,但它收到了一个JSON数组。通过解组到[]T而不是嵌套数组[T]进行修复。
Unrelated to the error, the method NestedArray[T].UnmarshalJSON does some unnecessary encoding and decoding. Fix by using json.RawMessage.
与错误无关,方法NestedArray[T].UnmarshalJSON执行了一些不必要的编码和解码。使用json.RawMessage修复。
Here's the code with both 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 the code on the playground!.
在操场上运行代码!
I got it it work by making the final unmarshal use an intermediary variable as opposed to the receiver of the method.
我让最终的解组使用一个中间变量,而不是方法的接收方,从而使其正常工作。
// ...
// Turn back into JSON and parse correctly
sliceJSON, err := json.Marshal(sliceVal)
if err != nil {
return err
}
// Instead of using n here, create a new variable and use that instead
var result []T
err = json.Unmarshal(sliceJSON, &result)
if err != nil {
return err
}
// Then, assign that to the receiver
*n = result
Loop through the JSON looking for the array. Decode each array element and append to the receiver.
循环遍历JSON以查找数组。对每个数组元素进行解码,并附加到接收器。
func (n *NestedArray[T]) UnmarshalJSON(data []byte) error {
d := json.NewDecoder(bytes.NewReader(data))
t, err := d.Token()
if err != nil {
return err
}
if t != json.Delim('{') {
return errors.New("object expected")
}
for d.More() {
// skip key
_, err = d.Token()
if err != nil {
return err
}
// Is it an JSON array?
t, err = d.Token()
if t == json.Delim('[') {
// Decode the array.
for d.More() {
var v T
err := d.Decode(&v)
if err != nil {
return err
}
*n = append(*n, v)
}
return nil
}
}
return nil
}
https://go.dev/play/p/lLFjJpr404W
Https://go.dev/play/p/lLFjJpr404W
The question's title is "Generic type alias to unmarshal struct as slice", but here is not a type alias in the question. A better title for the question is "Generic type definition to unmarshal struct as slice".
问题的标题是“将结构解组为切片的泛型类型别名”,但问题中没有类型别名。这个问题的更好标题是“将结构解组为切片的泛型类型定义”。
更多回答
Interesting, I didn't know about RawMessage
. Will keep it in mind for the future.
有趣的是,我不知道RawMessage。我会记住它的未来。
As mentioned in the post multiple times, the key is unknown. This pattern occurs in multiple places, each of them having a different key. I want a generic way to do it. Yours calls it item
which only works for the specific case in my example, but not all the others.
正如帖子中多次提到的那样,钥匙是未知的。这种模式出现在多个地方,每个地方都有不同的键。我想要一种通用的方法来做这件事。您的应用程序只对我的示例中的特定情况有效,而不是对其他所有情况都有效。
That's an interesting approach. In which aspects is this better or worse than what I posted myself as the answer?
这是一种有趣的方法。在哪些方面,这比我自己发布的答案更好或更差?
I was unaware of the other answer at the time I updated this answer. Here's my comparison now that I've seen the other answer: The other answer unmarshals, marshals and unmarshals again. This answer unmarshals once.
当我更新这个答案时,我并不知道另一个答案。下面是我的比较,因为我已经看到了另一个答案:另一个答案是解组,再解组,再解组。这个答案只有一次解组。
我是一名优秀的程序员,十分优秀!