Skip to content

Conversation

@pawannn
Copy link

@pawannn pawannn commented Nov 4, 2025

Problem

When binding a pflag to a nested key (e.g., "test.test_field") and then calling Sub("test"), the pflag binding was lost in the sub-Viper instance. This caused:

  1. AllKeys() on the sub-Viper doesn't include the pflag-bound key
  2. Unmarshal() on the sub-Viper doesn't populate fields bound to pflags
  3. Direct access returns the default value instead of the flag value

Related

Fixes #2089

Reproduction

Consider the following code:

package main

import (
	"log/slog"

	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

type TestConfig struct {
	TestField  bool `mapstructure:"test_field"`
	TestField2 bool `mapstructure:"test_field2"`
}

func main() {
	// Define the flag using pflag (recommended with Viper)
	pflag.Bool("test-flag", false, "Test flag description")

	// Parse the flags
	pflag.Parse()

	conf := viper.New()

	// Bind pflag to Viper
	_ = conf.BindPFlag("test.test_field", pflag.Lookup("test-flag"))

	conf.SetDefault("test.test_field", false)
	conf.Set("test.test_field2", true)

	subConf := conf.Sub("test")
	if subConf == nil {
		slog.Error("No sub conf")
		return
	}

	slog.Info("ALL Keys", "keys", subConf.AllKeys())

	var config TestConfig
	if err := subConf.Unmarshal(&config); err != nil {
		slog.Error("Error during unmarshal", "err", err)
	}

	slog.Info("TestField (Direct)", "val", conf.GetBool("test.test_field"))
	slog.Info("TestField", "val", config.TestField)

	slog.Info("TestField2 via Set (Direct)", "val", conf.GetBool("test.test_field2"))
	slog.Info("TestField2 via Set", "val", config.TestField2)
}

Output before Fix

Screenshot 2025-11-04 at 11 09 35 AM

Issues:

  • AllKeys() only shows [test_field2], missing test_field.
  • TestField unmarshals to false instead of true.
  • Direct access via parent works, but sub-viper doesn't.

Root Cause

The Sub() method only copied the stored config values, but didn't copy the pflag bindings that tell it where to get values from flags.

// Old implementation
func (v *Viper) Sub(key string) *Viper {
    subv := New()
    data := v.Get(key)
    if data == nil {
        return nil
    }
    if reflect.TypeOf(data).Kind() == reflect.Map {
        subv.config = cast.ToStringMap(data)  // !! This only copies config data
        // Missing: pflags, env, aliases, defaults, overrides
        return subv
    }
    return nil
}

Viper stores bindings separately:

  • v.pflags - pflag bindings
  • v.env - environment variable bindings
  • v.aliases - key aliases
  • v.defaults - default values
  • v.override - override values

When creating a sub-Viper, these binding maps were not copied for keys matching the subtree prefix, so the sub-Viper lost all context about bound flags.

Solution

Modified the Sub() method to:

  • Copy pflag bindings that have the requested key as a prefix
  • Copy env bindings that match the subtree
  • Copy aliases that reference keys in the subtree
  • Copy defaults and overrides from the subtree
  • Strip the prefix from copied keys to make them relative to the sub-Viper.

Changes

Modified Files
viper.go: Updated Sub() method to copy all binding types

Key Implementation Details

  • Use strings.CutPrefix() to efficiently check and remove prefixes.
  • Iterate through all binding maps.
  • Use searchMap() to extract nested defaults and overrides.
  • Preserve all Viper properties.
  • Maintain backwards compatibility - existing behavior unchanged.

Testing

Added comprehensive test coverage in viper_test.go:

Core Fix Tests

Binding Types

  • TestSubWithEnvBindings - Environment variable bindings
  • TestSubWithAliases - Alias preservation
  • TestSubWithDefaults - Default values

Integration & Edge Cases

  • TestSubWithMixedSources - Combining pflags, env, defaults, Set()
  • TestSubWithoutPFlagBindings - Backwards compatibility
  • TestSubReturnsNilForNonExistentKey - Nil for missing keys
  • TestSubWithCaseSensitivity - Case-insensitive handling
  • TestSubSharesConfigWithParent - Config sharing behavior
  • TestSubWithBoolPFlags - Boolean flag types
  • TestSubWithSlicePFlags - Array/slice flag types
  • TestSubWithUnsetPFlag - Flags using defaults
  • TestSubPriorityOrder - Value precedence (Set > PFlag > Default)
  • TestSubPreservesKeyDelimiter - Custom delimiter support
  • TestSubWithIntAndFloatPFlags - Numeric types
  • TestMultipleSubCallsIndependence - Multiple Sub() isolation

Output after Fix

Screenshot 2025-11-04 at 11 10 53 AM

Breaking Changes

None. This is a pure bug fix that adds missing functionality. All existing code continues to work as before.

  • Sub() without pflags works identically (tested)
  • Existing behavior preserved for all use cases
  • Only adds missing pflag/env/alias/defaults copying
  • No API changes or signature modifications

@CLAassistant
Copy link

CLAassistant commented Nov 4, 2025

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BindFlags does not work well with nested keys

2 participants