Preflight Checklist

  • [x] I have searched the issue tracker for an issue that matches the one I want to file, without success.
  • [x] I am not looking for support or already pursued the available support channels without success.
  • [x] I have checked the troubleshooting guide for my problem, without success.

Viper Version

1.20

Go Version

1.24.1

Config Source

Environment variables

Format

Other (specify below)

Repl.it link

https://replit.com/@jameel4/Viper-Struct-Unmarshaling?v=1

Code reproducing the issue

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "reflect"
    "strings"

    "github.com/go-viper/mapstructure/v2"
    "github.com/spf13/viper"
)

func jsonStringUnmarshallerHookFunc() mapstructure.DecodeHookFuncValue {
    return func(f reflect.Value, t reflect.Value) (any, error) {
        data := f.Interface()
        if f.Kind() != reflect.String || t.Kind() == reflect.String {
            return data, nil
        }
        raw := data.(string)
        if raw != "" && json.Valid([]byte(raw)) {
            var m any
            err := json.Unmarshal([]byte(raw), &m)
            return m, err
        }
        return data, nil
    }
}

type Config struct {
    Foo    string
    Nested struct {
        Bar string
    }
}

func main() {
    os.Setenv("FOO", "test")
    os.Setenv("NESTED", `{"bar": "test2"}`)

    var config Config
    v := viper.NewWithOptions(
        viper.ExperimentalBindStruct(),
        viper.WithDecodeHook(jsonStringUnmarshallerHookFunc()),
    )
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    v.AutomaticEnv()

    if err := v.Unmarshal(&config); err != nil {
        fmt.Println("Unable to unmarshal config")
    }
    fmt.Printf("%+v\n", config)

    if err := v.UnmarshalKey("nested", &config.Nested); err != nil {
        fmt.Println("Unable to unmarshal config")
    }
    fmt.Printf("%+v\n", config)
}

Expected Behavior

I expect the initial call to Unmarshal to correctly decode the NESTED environment variable into the Nested struct.

{Foo:test Nested:{Bar:test2}}
{Foo:test Nested:{Bar:test2}}

Actual Behavior

The NESTED environment variable in not decoded when calling Unmarshal, but it properly decoded when calling UnmarshalKey.

{Foo:test Nested:{Bar:}}
{Foo:test Nested:{Bar:test2}}

Steps To Reproduce

No response

Additional Information

No response

Comment From: github-actions[bot]

Issues with no activity for 30 days are marked stale and subject to being closed.

Comment From: sagikazarmark

This is an interesting edge case you have run into and sadly ties into the inconsistent internal model of Viper.

The problem is Viper tries to find a value for nested.bar by default, because that's the deepest key in the struct. But it fails to match to the env var that way.

The only workaround for now is v.BindEnv("NESTED"): simple and fixes your problem.