How to Unmarshall Viper config value to struct containing array of string properly?
Asked Answered
Q

1

5

I noticed that this is perhaps a bug when viper tries to unmarshall to struct. To explain it better, consider this:

I have a cli command like below dd-cli submit-bug --name "Bug 1" --tag reason1 --tag reason2

Here is my command line source code

package cmd

import (
    "fmt"

    "github.com/spf13/viper"

    "github.com/spf13/cobra"
)

// SubmitBugOpts is a set of flags being exposed by this Deploy command
type SubmitBugOpts struct {
    Name string `mapstructure:"bug-name"`

    ReasonTags []string `mapstructure:"tags"`
}

var (
    submitBugOpts = SubmitBugOpts{}
)

func submitBugRun(cmd *cobra.Command, args []string) {
    fmt.Printf("Bug Name is %+v\n", submitBugOpts.Name)
    fmt.Printf("List of tags is %+v\n", submitBugOpts.ReasonTags)
    fmt.Printf("Length of tags is %d\n", len(submitBugOpts.ReasonTags))
    for index, el := range submitBugOpts.ReasonTags {
        fmt.Printf("tag[%d] = %s\n", index, el)
    }
}

var submitBugCmd = &cobra.Command{
    Use:   "submit-bug",
    Short: "Deploy/Install a helm chart to Kubernetes cluster",
    Run:   submitBugRun,
    PreRun: func(cmd *cobra.Command, args []string) {
        pFlags := cmd.PersistentFlags()
        viper.BindPFlag("bug-name", pFlags.Lookup("name"))
        viper.BindPFlag("tags", pFlags.Lookup("tag"))

        fmt.Printf("Viper all setting value: %+v\n", viper.AllSettings())
        fmt.Printf("Before unmarshall: %+v\n", submitBugOpts)
        viper.Unmarshal(&submitBugOpts)
        fmt.Printf("After unmarshall: %+v\n", submitBugOpts)
    },
}

func init() {
    rootCmd.AddCommand(submitBugCmd)

    pFlags := submitBugCmd.PersistentFlags()
    pFlags.StringVar(&submitBugOpts.Name, "name", "", "the bug name")
    pFlags.StringArrayVar(&submitBugOpts.ReasonTags, "tag", nil, "the bug's reason tag. You can define it multiple times")

    submitBugCmd.MarkPersistentFlagRequired("name")
    submitBugCmd.MarkPersistentFlagRequired("tag")
}

I run this command:

dd-cli submit-bug --name "Bug 1" --tag reason1 --tag reason2

And the output is below

Viper all setting value: map[bug-name:Bug 1 tags:[reason1,reason2]]
Before unmarshall: {Name:Bug 1 ReasonTags:[reason1 reason2]}
After unmarshall: {Name:Bug 1 ReasonTags:[[reason1 reason2]]}
Bug Name is Bug 1
List of tags is [[reason1 reason2]]
Length of tags is 2
tag[0] = [reason1
tag[1] = reason2]

I expect the viper.Unmarshall() will correctly omit the [ for submitBugOpts.ReasonTags [0] and omit the ] for submitBugOpts.ReasonTags[1]. So the expected value of submitBugOpts.ReasonTags doesn't contains any [ and ] .

Any pointer how to fix this? I've submitted this issue on viper repo: https://github.com/spf13/viper/issues/527. However I am asking on SO just in case you guys know how to handle this too.

Querulous answered 9/7, 2018 at 13:11 Comment(2)
Maybe I miss something or you unmarshaling something that is already unmarshalled? Or what is the reason to make it because values are already properly binded to structSubreption
@Subreption this is because the value coming from (viper) co fig file only binds to the viper instance, not to struct. I want to decouple my app logic from viper, that is why i want to unmarshall viper values to the structQuerulous
S
6

After digging into codes of github.com/spf13/{cobra,viper,pflag}for quite a while, I finally find the problem.

When you call pFlags.StringArrayVar(&submitBugOpts.ReasonTags, "tag", nil, ...), the ReasonTags, of course, binds to a wrapper of pflag.stringArrayValue. source.

And when you call viper.Unmarshall, viper uses v.Get to get the value binds to ReasonTags and then it calls v.find.

In v.find, after the value is found, it uses the wrapper's ValueType() method to determine its type, the wrapper then calls the Type method of the wrapped type, pflag.stringArrayValue, and returns "stringArray". source

But viper only handle "stringSlice" as a special case, so the value gets to default part of type switch, which uses its ValueString() method - making it into a string, with "[" and "]" on both side. source

And when finally unmarshalling, as your output param, ReasonTags is of []string, the program simply splitted the string and set it into the field.

As for solution, if you are ok with prohibitting tag to contain ,, simply change StringArrayVar to StringSliceVar, but this will results in --tag "Yet, the problem re-occurs" into []string{"Yet"," the problem re-occrus"}.

If that is critical, you'll need to ask the developers of viper to create a case for stringArray.

Sonatina answered 13/7, 2018 at 15:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.