Home > Net >  Golang Json decode validation won't raise error with interface
Golang Json decode validation won't raise error with interface

Time:09-14

I am a total noob with Golang and would really appreciate any help on the following

I had this code snippet which was working fine

var settings CloudSettings

type CloudSettings struct {
...
    A1 *bool `json:"cloud.feature1,omitempty"`
...
}

err = json.NewDecoder(request.Body).Decode(&settings)

An attempt to send an invalid string would raise this error:

curl ... -d '{"cloud.feature1" : "Junk"}'

"message":"Error:strconv.ParseBool: parsing \"Junk\": invalid syntax Decoding request body."

Now, we have a separate LocalSettings struct and the same function needs to handle cloud/local setting decoding conditionally

So, the code was changed to:

var settings interface{} = CloudSettings{}

// If the request header says local settings
settings = LocalSettings{}

/* After this change Decode() no longer raises any error for invalid strings and accepts anything */
err = json.NewDecoder(request.Body).Decode(&settings)

So the question is why do I see this behavior and how would I fix this ?

If I have 2 separate settings variables, then the entire code from that point onwards would just be duplicated which I want to avoid

CodePudding user response:

In the second snippet, you have an interface initialized to a struct, but passing address of that interface. The interface contains a LocalSettings or CloudSetting value, which cannot be overwritten, so the decoder creates a map[string]interface{}, sets the value of the passed interface to point to that, and unmarshal data. When you run the second snippet, you are not initializing the local settings or cloud settings.

Change:

settings=&CloudSettings{}

or

settings=&LocalSettings{}

and

err = json.NewDecoder(request.Body).Decode(settings)

and it should behave as expected

CodePudding user response:

Based on your question, I'm assuming all fields (even the ones with the same name) have a cloud. or local. prefix in the JSON tags. If that's the case, you can simply embed both options into a single type:

type Wrapper struct {
    *CloudSettings
    *LocalSettings
}

Then unmarshal into this wrapper type. The JSON tags will ensure the correct field on the correct settings type are populated:

wrapper := &Wrapper{}
if err := json.NewDecoder(request.Body).Decode(&wrapper); err != nil {
    // handle
}
// now to work out which settings were passed:
if wrapper.CloudSettings == nil {
    fmt.Println("Local settings provided!")
    // use wrapper.CloudSettings
} else {
    fmt.Println("Cloud settings provided!")
    // use wrapper.LocalSettings
}

Playground demo

You mention that we expect to see local settings loaded based on a header value. You can simply unmarshal the payload, and then check whether the header matches the settings type that was loaded. If the header specified local settings, but the payload contained cloud settings, simply return an error response.

Still, I'm assuming here that your JSON tags will be different for both setting types. That doesn't always apply, so if my assumption is incorrect, and some fields share the same JSON tags, then a custom Unmarshal function would be the way to go:

func (w *Wrapper) UnmarshalJSON(data []byte) error {
    // say we want to prioritise Local over cloud:
    l := LocalSettings{}
    if err := json.Unmarshal(data, &l); err == nil {
        // we could unmarshal into local without a hitch?
        w.CloudSettings = nil // ensure this is blanked out
        w.LocalSettings = &l // set local
        return nil
    }
    // we should use cloud settings
    c := CloudSettings{}
    if err := json.Unmarshal(data, &c); err != nil {
        return err
    }
    w.LocalSettings = nil
    w.CloudSettings = &c
    return nil
}

This way, any conflicts are taken care of, and we can control which settings take precedence. Again, regardless of the outcome of the JSON unmarshalling, you can simply cross check the header value which settings type was populated, and take it from there.

Lastly, if there's a sizeable overlap between both settings types, you could just as well unmarshal the payload into both types, and populate both fields in the wrapper type:

func (w *Wrapper) UnmarshalJSON(data []byte) error {
    *w = Wrapper{} // make sure we start with a clean slate
    l := LocalSettings{}
    var localFail err
    if err := json.Unmarshal(data, &l); err == nil {
        w.LocalSettings = &l // set local
    } else {
        localFail = err
    }
    c := CloudSettings{}
    if err := json.Unmarshal(data, &c); err == nil {
        w.CloudSettings = &c
    } else if localFail != nil { // both unmarshal calls failed
        return err // maybe wrap/return custom error
    }
    return nil // one or more unmarshals were successful
}

That should do the trick

  • Related