0rka
0rka

Reputation: 2386

UnmarshalJSON on struct containing interface list

I would like to UnmarshalJSON a struct containing an interface as follows:

type Filterer interface {
    Filter(s string) error
}

type FieldFilter struct {
    Key string
    Val string
}    

func (ff *FieldFilter) Filter(s string) error {
    // Do something
}

type Test struct {
    Name string
    Filters []Filterer
}

My idea was to send a json like so:

{
    "Name": "testing",
    "Filters": [
        {
            "FieldFilter": {
                "Key": "key",
                "Val": "val"
            }
        }
    ]
}

However, when sending this json to the unmarshaler, the following exception returns: json: cannot unmarshal object into Go struct field Test.Filters of type Filterer

I understand the problem fully, but do not know how to approach this problem wisely. Looking for advice on an idiomatic way to solving this problem in go.

Upvotes: 0

Views: 900

Answers (2)

0rka
0rka

Reputation: 2386

Following my own question, I researched how one could implement UnmarshalJSON for interface lists. Ultimately this led me to publish a blog post on how to do this properly. Basically there are 2 main solutions:

  1. Parse the required JSON string into a map[string]*json.RawMessage and work your way from there.
  2. Make an alias for the interface list and implement UnmarshalJSON for that alias. However, you'll still need to work with map[string]*json.RawMessage and some manual work. Nothing comes without a price!

I highly suggest taking the seconds approach. While these two solutions may result in the same amount of code lines, taking advantage of type aliasing and being less dependent on json.RawMessage types will make a more easy to manage code, especially when it is required to support multiple interfaces on the UnmarshalJSON implementation

To directly answer the question, start with making a type alias for the interface list:

type Filterers []Filterer

Now continue with implementing the decoding of the JSON:

func (f *Filterers) UnmarshalJSON(b []byte) error {
    var FilterFields map[string]*json.RawMessage
    if err := json.Unmarshal(b, &FilterFields); err != nil {
        return err
    }
    for LFKey, LFValue := range FilterFields {
        if LFKey == "FieldFilter" {
            var MyFieldFilters []*json.RawMessage
            if err := json.Unmarshal(*LFValue, &MyFieldFilters); err != nil {
                return err
            }
            for _, MyFieldFilter := range MyFieldFilters {
                var filter FieldFilter
                if err := json.Unmarshal(*MyFieldFilter, &filter); err != nil {
                    return err
                }
                *f = append(*f, &filter)
            }
        }
    }
    return nil
}

A detailed explanation (with some examples and a full working code snippets) of the second approach is available on my own blog

Upvotes: 4

Adrian
Adrian

Reputation: 46432

There is no way for Unmarshal to know what type it should use. The only case where it can just "make something up" is if it's asked to unmarshal into an interface{}, in which case it will use the rules in the documentation. Since none of those types can be put into a []Filterer, it cannot unmarshal that field. If you want to unmarshal into a struct type, you must specify the field to be of that type.

You can always unmarshal into an intermediate struct or map type, and then do your own conversion from that into whatever types you want.

Upvotes: 2

Related Questions