Golang结构体用于JSON,允许值可选为数组

7

我正在尝试更改 AWS 上 S3 存储桶的策略。我已创建了以下 JSON 结构用于策略:

type Policy struct {
    Version     string  `json:"Version"`
    Id          string  `json:"Id"`
    Statement   []Statement `json:"Statement"`
}

type Statement struct {
    Sid         string      `json:"Sid"`
    Effect      string      `json:"Effect"`
    Principal   Principal   `json:"Principal"`
    Action      []string    `json:"Action"`
    Resource    []string    `json:"Resource"`
}

type Principal struct {
    AWS[]string `json:"AWS"`
}

在设置存储桶策略时,一切都很顺利。问题出现在我尝试获取当前策略并修改它时。

如果有一个语句只有一个AWS、Action或Resource值,Amazon会将其从数组转换为简单值,导致我的取消编组失败。

有没有办法可以指定AWS/Action/Resource值为字符串切片或仅为字符串?


我知道有可用的包可以在某种程度上解决这个问题(例如github.com/Jeffail/gabs),但直接创建JSON结构会更清晰,因为它非常简单。


你能提供两个JSON字符串的例子吗? - huygn
1
顺便提一下,所有这些json标签都是不必要的,因为它们恰好与字段名称匹配。如果类型上没有json标签,则将使用字段名称。 - voutasaurus
我不确定是否需要使用json标签,但我猜想只有在实现中需要为字段命名不同的名称时才需要它们。 - arewm
嘿,你能用下面的解决方案解决这个问题吗?我的意思是这个“AWS []string”是MaybeSlice还是你有相关代码? - mshikher
2个回答

3
作为interface{}的替代方案,你可以创建一个名为MaybeSlice的类型,并在其上实现自定义的MarshalJSON和UnmarshalJSON方法。
type MaybeSlice []string

func (ms *MaybeSlice) MarshalJSON() ([]byte, error) {
    // Use normal json.Marshal for subtypes
    if len(*ms) == 1 {
        return json.Marshal(([]string)(*ms)[0])
    }
    return json.Marshal(*ms)
}

func (ms *MaybeSlice) UnmarshalJSON(data []byte) error {
    // Use normal json.Unmarshal for subtypes
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        var v []string
        if err := json.Unmarshal(data, &v); err != nil {
             return err
        }
        *ms = v
        return nil
    }
    *ms = []string{s}
    return nil
}

通过实现这些方法,MaybeSlice类型将满足json.Marshal和json.Unmarshal期望的接口,因此您不需要为所有类型实现自定义编组器,只需为此类型实现即可。
MaybeSlice是一个糟糕的名称,但希望您能理解。
对于结构体类型,这些自定义方法更容易实现,但我认为上述内容是正确的。如果我没记错的话,您需要使Action成为*MaybeSlice才能使用上述方法。

做得很好,就像我想要的一样,谢谢!我开始自己摸索,但是可能需要更长的时间。 :) - arewm
嗨@voutasaurus,我不明白Action结构体如何知道要调用哪个Marshal方法?你能提供任何支持性文章来理解上述实现方案背后的概念吗? - mshikher
@mshikher 请参考其他答案以获取上下文。在Action字段中,您可以使用上面定义的MaybeSlice类型,而不是使用[]string或interface{}类型。标准库json包会调用自定义的marshal和unmarshal函数,因为它检查类型是否实现了该包中定义的Marshaller和Unmarshaller接口。 - voutasaurus

1

如果要解析的字段类型不确定,请使用interface{}

type Statement struct {
    Sid       string    `json:"Sid"`
    Effect    string    `json:"Effect"`
    Principal Principal `json:"Principal"`

    Action   interface{} `json:"Action"`
    Resource interface{} `json:"Resource"`
}

使用类型切换访问底层原始数据类型:

//Example: Trying to access Action member of a statement myStatement.
switch a := myStatement.Action.(type) {
    case []string:
        //Action is a slice. Handle it accordingly.
    case string:
        //Action is a string. Handle it accordingly.
    default:
        //Some other datatype that can be returned by aws?
}

或者你可以为两种情况分别创建不同的结构体,如果将其中一个反序列化失败,就将其反序列化到另一个结构体中,类似于这样:
err := json.Unmarshal(jsonStr, &struct1)
if err != nil {
    fmt.Println(err)
    err = json.Unmarshal(jsonStr, &struct2)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接