Unmarshaling INI file using Viper Go is not working as expected

467 Views Asked by At

I am using viper for app configuration.

Code sample is below.

package main

import "github.com/spf13/viper"

type Config struct {
    WelcomeMessage string `mapstructure:"message"`
}

func main() {

    viper.SetConfigName("config")
    viper.SetConfigType("ini")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig() // Find and read the config file
    if err != nil {             // Handle errors reading the config file
        //handle error
    }

    // var result map[string]interface{}
    config := Config{}

    err = viper.Unmarshal(&config)
    if err != nil {
        //handle error
    }
    ...

}

INI file is simple with single config.

message=Welcome!

I can read configuration value using viper.Get() if I use default.message as key.

For some reason, Unmarshal is not working, meaning that there config variable is struct without value set in WelcomeMessage field.

What am I doing wrong?

Thanks.

3

There are 3 best solutions below

4
Kurtis Rader On BEST ANSWER

The https://github.com/spf13/viper project is woefully under documented. Looking at the source I figured out what is needed to make your code work. The key insight is that the key you are trying to unmarshal is implicitly in a section named "default". So your structure has to put the corresponding variable inside a structure named "Default".

I started by creating a file named config.ini with this content:

message=Welcome!
hello = goodbye

I then put the following code in a file named x.go:

package main

import (
        "fmt"

        "github.com/spf13/viper"
)

type Config struct {
        Default struct {
                WelcomeMessage string `mapstructure:"message"`
                Hello          string
                Undefined      string
        }
}

func main() {

        viper.SetConfigName("config")
        viper.SetConfigType("ini")
        viper.AddConfigPath(".")
        err := viper.ReadInConfig() // Find and read the config file
        if err != nil {             // Handle errors reading the config file
                panic(err)
        }

        // var result map[string]interface{}
        config := Config{}

        err = viper.Unmarshal(&config)
        if err != nil {
                panic(err)
        }

        fmt.Printf("%#v\n", config)
}

Executing "go run x.go" produces this output (which matches what you expect, modulo the need for a nested struct):

main.Config{Default:struct { WelcomeMessage string "mapstructure:\"message\""; Hello string; Undefined string }{WelcomeMessage:"Welcome!", Hello:"goodbye", Undefined:""}}

P.S., Having gone to the trouble of solving this question, and looking at the Viper project source code (including its unit tests, API and documentation) I do not recommend using that project.

P.P.S., The INI file format is like the CSV format. Both were created by Microsoft and were horribly underspecified at the time of their introduction. Resulting in many ambiguities and incompatible implementations. Both formats should be avoided.

2
Kurtis Rader On

Assuming viper refers to https://pkg.go.dev/github.com/dvln/viper then you should read its documentation:

Q: Why not INI files?

A: Ini files are pretty awful. There’s no standard format, and they are hard to validate. Viper is designed to work with JSON, TOML or YAML files. If someone really wants to add this feature, I’d be happy to merge it. It’s easy to specify which formats your application will permit.

0
Zeke Lu On

After viper.ReadInConfig is called, the config is stored in a map like this:

settings := map[string]any{
    "default": map[string]any{
        "message": "Welcome!",
    },
}

So the struct should be defined like this:

type Config struct {
    Default struct {
        WelcomeMessage string `mapstructure:"message"`
    }
}

This is the full example:

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

type Config struct {
    Default struct {
        WelcomeMessage string `mapstructure:"message"`
    }
}

func main() {
    viper.SetConfigName("config")
    viper.SetConfigType("ini")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }

    // To see what is stored.
    fmt.Printf("%#v\n", viper.AllSettings())

    config := Config{}

    err = viper.Unmarshal(&config)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%+v\n", config)
}

And the output:

map[string]interface {}{"default":map[string]interface {}{"message":"Welcome!"}}
{Default:{WelcomeMessage:Welcome!}}