viper: 1.18.0 Unmarshal() Breaking Our Tests With: '' expected a map, got 'interface'

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

1.18.0

Go Version

1.21.5

Config Source

Manual set

Format

Other (specify below)

Repl.it link

No response

Code reproducing the issue

No response

Expected Behavior

For now, due to the urgency of this new release having come out only yesterday and limited time (until maybe this weekend?), I can just say that our unit tests (for two modules, within a package we have to wrap/provide common Golang functions) have been running fine for awhile, and with this v1.18.0 release we’re getting these errors from Viper.Unmarshal().

Actual Behavior

Sample error:

    config_test.go:146: 
        	Error Trace:	/Users/c/go/src/github.com/namely/go-common/config/config_test.go:146
        	Error:      	Received unexpected error:
        	            	Viper.Unmarshal returned: '' expected a map, got 'interface'
        	Test:       	TestConfigTestSuite/TestBind

Steps To Reproduce

No response

Additional Information

I checked the release notes and didn’t see anything obvious, but the code diffs between 1.17.0 and 1.18.0 do show changes in the decoding code that seems to be involved here.

I’m hoping this limited (information-poor) Issue submission might help someone quickly figure out what’s wrong, if anything is indeed wrong with the new release (versus, say, a latent bug in our code, or at least test code).

If not, I’ll endeavor to reduce our layers of wrapping to a simple test case showing the issue, and perhaps “get lucky”, find the bug, and submit a fix as well.

About this issue

  • Original URL
  • State: open
  • Created 7 months ago
  • Reactions: 7
  • Comments: 29 (8 by maintainers)

Most upvoted comments

@krakowski thanks for the feedback.

How about this: Let’s release 1.18.2 with the build flag solution so that people can go back to the original functionality ASAP.

Then we can tag 1.19 with a runtime version. I’d like to sleep on the API though.

I was able to work around this in our code by changing from:

	if err = viper.Unmarshal(&dst); err != nil {

To:

	if err = viper.Unmarshal(dst); err != nil {

The dst variable there is of type interface{}.

me too

The ‘’ expected a map, got ‘interface’ seems strange to me, I’m not sure how to replicate it and how it would have worked before.

@sagikazarmark Here’s a repro for the error:

func demoConfig(conf *globalConfig) error {
	viper.SetConfigType("yaml")
	if err := viper.ReadConfig(strings.NewReader(demoYaml)); err != nil {
		return fmt.Errorf("failed decoding demo config: %w", err)
	}

	if err := viper.UnmarshalExact(&conf); err != nil {
		return fmt.Errorf("failed loading demo config: %w", err)
	}

	return nil
}

I used to pass a pointer to the global config struct and indirected that once more when calling Viper. Seems that is unnecessary. Passing to viper.UnmarshalExact by value should be ok since a copy of the conf pointer would still point to the same globalConfig. However- that worked before.

globalConfig looks like this:

type globalConfig struct {
	Network      networkConfig
	Log          string
	SponsorToken string
	Plant        string // telemetry plant id
	Telemetry    bool
	Metrics      bool
	Profile      bool
	Levels       map[string]string
	Interval     time.Duration
	Database     dbConfig
	Mqtt         mqttConfig
	ModbusProxy  []proxyConfig
	Javascript   []javascriptConfig
	Go           []goConfig
	Influx       server.InfluxConfig
	EEBus        map[string]interface{}
	HEMS         config.Typed
	Messaging    messagingConfig
	Meters       []config.Named
	Chargers     []config.Named
	Vehicles     []config.Named
	Tariffs      tariffConfig
	Site         map[string]interface{}
	Loadpoints   []map[string]interface{}
}

A couple thoughts:

  • passing a pointer of a pointer does not make much sense to me, but your example @juliusbachnick, actually shows you may not even be aware that you are doing so, so I’m inclined to say we should handle it gracefully
  • given Unmarshal is not fatally broken, I don’t feel the need to revert the change for an immediate fix. 1.18 does not actually come with that many changes, and there are workarounds for the problem.
  • I think the pointer-of-pointer situation should be handled by mapstructure. I’m gonna open an issue/PR to address that, but given the last release of mapstructure was more than a year ago, we will probably have to workaround it.

I have a very rudimentary quick fix in https://github.com/spf13/viper/pull/1707. If you could give that a try, that would be great.

It would also be great to understand if there are other cases where mapstructure fails.

The '' expected a map, got 'interface' seems strange to me, I’m not sure how to replicate it and how it would have worked before.

Have also been bitten by this, but in my case we’re not passing a pointer to a pointer, but rather a generic type:

	var config T
	if err = c.Unmarshal(&config); err != nil {
		panic(fmt.Errorf("failed to unmarshal configs: %w", err))
	}

The actual type we pass in to this is a plain struct.

I had a similar issue which I think is due to the additional step when unmarshalling as introduced in #1429 which tries to decode the provided interface{} into a map using mapstructure’s decode which later on fails here when trying to decode the data (i.e. the input interface{} to val i.e. the output value which is the map. The failure happens because the input interface{}'s kind can only be one of the supported types of the switch statement and a pointer is not supported.

Fwiw, I fixed this by ensuring that I just pass a pointer to Unmarshal (and not a pointer to a pointer or something similar).

We’re seeing the same here.

config_test.go:42: '' expected a map, got 'interface'