#2146·viper

Unmarshal() does not preserve nested configuration when all fields are YAML null

Author: sk-wangwyCreated Sep 3, 2026Updated Sep 3, 2026
Labelskind/bug

Preflight Checklist

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

Viper Version

v1.20.0

Go Version

1.24.0

Config Source

Files

Format

YAML

Repl.it link

No response

Code reproducing the issue

go
package main

import (
	"bytes"
	"fmt"

	"github.com/spf13/viper"
)

type clientConfig struct {
	Protocol string `mapstructure:"protocol"`
	Addr     string `mapstructure:"addr"`
}
type Config struct {
	Clients map[string]clientConfig `mapstructure:"clients"`
}

func main() {
	data := `
clients: 
  mysql: 
    protocol: 
    addr:
`
	v := viper.New()
	v.SetConfigType("yaml")

	if err := v.ReadConfig(bytes.NewBufferString(data)); err != nil {
		panic(err)
	}

	var cfg Config
	if err := v.Unmarshal(&cfg); err != nil {
		panic(err)
	}

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

Expected Behavior

Output: map[string]main.clientConfig{ "mysql": {}, }

Actual Behavior

Output: map[string]main.clientConfig(nil)

Steps To Reproduce

  1. Use the YAML configuration shown above.
  2. Call v.Unmarshal(&cfg).
  3. Check cfg.Clients.

Additional Information

The issue appears to occur in AllSettings(), before the configuration is passed to mapstructure for unmarshaling.

  • v.Get("clients") correctly returns the mysql entry with nil values.
  • v.Get("clients.mysql") also correctly returns the configuration.
  • However, v.AllSettings() omits the clients section entirely.
  • As a result, Unmarshal() has no clients.mysql entry to decode into the destination map.

I could not find a decoder option that preserves this entry, since the entry is already missing from AllSettings() before decoding occurs.