Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang structure for json to allow a value to optionally be an array

Tags:

json

go

amazon-s3

I am trying to change the policy for an s3 bucket on aws. I have created the following json structure for a policy:

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"`
}

Which works fine for putting bucket policies in place. The issue comes when I try to get the current policy and modify it.

If there is a statement that only has one AWS, Action, or Resource value, Amazon will convert it from an array to a simple value, causing my unmarshalling to fail.

Is there any way that I can specify AWS/Action/Resource values to be either a string slice or just a string?


I know that there are packages available that I could use to get around this to some extent (github.com/Jeffail/gabs, for example), but it would be cleaner to just create the JSON structure since it is fairly simple.

like image 329
arewm Avatar asked Aug 03 '16 17:08

arewm


2 Answers

As an alternative to interface{}, you can create a type called MaybeSlice and implement custom MarshalJSON and UnmarshalJSON methods on it.

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
}

By implementing these methods the MaybeSlice type will satisfy the interfaces expected by json.Marshal and json.Unmarshal so you won't need to implement custom marshallers for all your types, just this one.

MaybeSlice is a terrible name but hopefully you get the idea.

These custom methods are easier with struct types, but I think the above is correct. If I remember correctly you need to make Action a *MaybeSlice to use the above.

like image 129
voutasaurus Avatar answered Oct 07 '22 19:10

voutasaurus


Use interface{} if the field's type you're going to parse is not guaranteed:

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

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

And access the underlying original datatype using a type switch:

//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?
}

Or you can have separate structs for both cases, if Unmarshaling one failed, Unmarshal it into the other struct, something like this:

err := json.Unmarshal(jsonStr, &struct1)
if err != nil {
    fmt.Println(err)
    err = json.Unmarshal(jsonStr, &struct2)
}
like image 21
huygn Avatar answered Oct 07 '22 19:10

huygn