Open jcburley opened 7 months ago
👋 Thanks for reporting!
A maintainer will take a look at your issue shortly. 👀
In the meantime: We are working on Viper v2 and we would love to hear your thoughts about what you like or don't like about Viper, so we can improve or fix those issues.
⏰ If you have a couple minutes, please take some time and share your thoughts: https://forms.gle/R6faU74qPRPAzchZ9
📣 If you've already given us your feedback, you can still help by spreading the news, either by sharing the above link or telling people about this on Twitter:
https://twitter.com/sagikazarmark/status/1306904078967074816
Thank you! ❤️
We're seeing the same here.
config_test.go:42: '' expected a map, got 'interface'
me too
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).
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.
Related issue here: after upgrading to 0.18.1 (which changes UnmarshalExact
) I'm seeing:
'' expected a map, got 'ptr'
/cc @krakowski in case this related to #1429 or #1704
Thanks for the analysis @juliusbachnick !
As a first course of action, we could disable the new struct binding feature by default to make sure Viper continues working.
It would also help if people with issues using the new struct binding bit could share what they are trying to unmarshal to.
From what I understand pointer of a pointer doesn't work. Are there others?
(I'd also like to understand what the use case is for passing a pointer to a pointer)
We're seeing the same here.
config_test.go:42: '' expected a map, got 'interface'
@shane-ns1 could you provide a snippet of your code. I have no idea how unmarshaling to an interface would work.
Here is a rough first workaround, but I believe mapstructure should take care of this: #1707
(I'd also like to understand what the use case is for passing a pointer to a pointer)
Apologies, I wasn't clear on my side. In my particular case, the code that invoked viper.Unmarshal
used the pointer to an implementation of a provided interface
but the passed argument already was a pointer, similar to
package main
import (
"fmt"
"github.com/spf13/viper"
)
type Echo struct{}
func (e *Echo) String() string { return "echo" }
func main() {
e := &Echo{}
if err := doSomething(e); err != nil {
panic(err)
}
}
func doSomething(s fmt.Stringer) error {
return viper.Unmarshal(&s)
}
So this actually was unintended, but somehow this worked previously. If anything, this change revealed the bug in my code :).
Hello, same here, I encountered a bug when using viper.Unmarshal
, with 1.18 & 1.18.1 versions.
The error :
mustReadConfig: '' expected a map, got 'ptr'
The issue arises when I have the following code:
// C is a concrete implementation of Module interface
C struct {
parser string
cfg *models.Cfg
logger logger.Logger
// mustReadConfig reads a config file.
func (c *C) mustReadConfig(reader io.Reader) {
switch c.parser {
case "json", "yaml", "yml", "toml":
viper.SetConfigType(c.parser)
default:
c.logger.Fatalf("unsupported parsing format for config file %v\n", c.parser)
}
if err := viper.ReadConfig(reader); err != nil {
c.logger.Fatalf("mustReadConfig: %v", err)
}
if err := viper.Unmarshal(&c.cfg); err != nil {
c.logger.Fatalf("mustReadConfig: %v", err)
}
}
The bug occurs with the pointer models.Cfg in the C struct. The issue is related to the use of Unmarshal.
However, the bug disappears when I remove the pointer from models.Cfg.
Alternatively, using viper.UnmarshalExact
instead of viper.Unmarshal
also resolves the issue & working with the pointer.
A couple thoughts:
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.
@sagikazarmark I think the issue here is that nothing has changed with regards to the decoding target (i.e. from user side). What has imho changed is additional inputs (from ENV) into the decoding process. Hence I think that this must somehow be due to https://github.com/spf13/viper/pull/1429 or https://github.com/spf13/viper/pull/1704 and not due to mistakes on the part of Viper users?
Where does that pointer-to-pointer come from that did not present a problem before?
That said, https://github.com/spf13/viper/pull/1707 seems to work for me. No more
'' expected a map, got 'ptr'
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{}
}
@andig it's definitely related to #1429, but that doesn't mean it's not the result of some nonsensical usage, that worked as side-effect so far.
But I'm not trying to blame it on Viper users if that's what you are getting at. I'm simply trying to gather information to decide the best course of action.
Absolutely, please excuse me if I gave the impression of feeling blamed. I‘m super thankful for your work!
@andig no worries! ;)
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{}
.
Thanks @shane-ns1 et al — though I haven't investigated this fully myself so as to understand it completely, your comments have given me some idea of what to try.
Since our code that calls viper.Unmarshal()
is itself a wrapper called in a variety of scenarios (certainly by our unit tests), removing the leading &
fixed some tests and broke others. I'm hesitant to simply dismiss (EOL) the newly failing tests, since they might represent real code elsewhere in our org, and came up with this (paraphrased a bit) instead:
// Support callers that pass pointers to structs as well as
// just structs to be populated, without doing
// double-indirection that ran afoul of viper 1.18.0.
val := reflect.ValueOf(dst)
if val.Kind() != reflect.Ptr && val.CanAddr() {
dst = val.Addr().Interface()
}
// Unmarshal the config into the supplied struct, using `config` as the tag
// name
err = viper.Unmarshal(dst, func(...) { ... })
I had to change two of our tests to reflect other behavioral changes in viper
. One such change seemed entirely reasonable; the other totally mystifies me as to how it ever worked. I might post more about either/both here or, if I feel strongly they might represent regressions, as new Issue(s).
I've opened a PR to disable struct binding by default for now until we feel relatively confident that we found all backwards incompatibilities: https://github.com/spf13/viper/pull/1715
You can enable struct binding by adding the viper_bind_struct
to the list of your build tags.
Would this work for everyone while we figure out a safe way to enable the feature again?
If so, I'm going to release it as 1.18.2
cc @krakowski
@sagikazarmark Is it possible to make this a runtime option (defaulting to false)? Something like experimental opt-in flags. This way people who want to use it don't have to recompile the library themselves.
In the long run a function for extracting all fields using reflect
will be needed for this to work reliably, as mapstructure
's decode
function can't be used to extract nested fields which must be accessed through a pointer.
Would this work for everyone while we figure out a safe way to enable the feature again?
@sagikazarmark situation is a bit unfortunate, but still: feature/breakage was introduced with 1.18.0, which would already indicate some attention when upgrading. Root cause is unnecessary use of pointer to pointer (at least in my case). Plus, https://github.com/spf13/viper/pull/1707 would already fix the issue.
I would like to propose to merge #1707 instead of adding a build tag and release that?
@krakowski adding a runtime flag would require introducing something to the API. Removing that would be considered a BC break. Some could argue that using build tags for the same purpose is no different, but so far it seemed like a better solution.
Also, it is temporary until we figure out how to resolve the BC problems.
Do you have a scenario in mind where build tags would be suboptimal?
In the long run a function for extracting all fields using
reflect
will be needed for this to work reliably, asmapstructure
'sdecode
function can't be used to extract nested fields which must be accessed through a pointer.
Can you elaborate? A single pointer is not a problem. A pointer to a pointer is. I agree this should be solved in mapstructure though.
@andig My concern is that #1707 doesn't resolve the problem entirely. I've seen other error messages reported in this issue. Until we can figure those out, I'm more comfortable with not enabling the new feature by default.
@krakowski adding a runtime flag would require introducing something to the API. Removing that would be considered a BC break. Some could argue that using build tags for the same purpose is no different, but so far it seemed like a better solution.
What I had in mind here is a simple map[string]bool
and viper.EnableFeature(string)
. This could allow to enable (experimental) features dynamically at runtime in the future and get feedback on them from the community.
Also, it is temporary until we figure out how to resolve the BC problems.
Do you have a scenario in mind where build tags would be suboptimal?
Personally, this would work for me so I could only come up with an artificial scenario here: Using two viper instances where just one of them has the feature enabled.
In the long run a function for extracting all fields using
reflect
will be needed for this to work reliably, asmapstructure
'sdecode
function can't be used to extract nested fields which must be accessed through a pointer.Can you elaborate? A single pointer is not a problem. A pointer to a pointer is. I agree this should be solved in mapstructure though.
Sure. I made a comment within the original PR describing what goes wrong, when using pointer fields inside structs. Please have a look at https://github.com/spf13/viper/pull/1429#issuecomment-1856118406
@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 ended up forking mapstructure and will maintain it under go-viper, so we may have a more permanent solution soon.
As discussed above, I tagged 1.18.2 disabling the new bind struct feature. It can be enabled with the viper_bind_struct
tag.
I will add a new interface for enabling the feature runtime in 1.19.
I'd still like to look into what causes the same error with interface{}
before enabling this feature by default again, but I don't think it's gonna happen this year.
I may still add the runtime optional feature interface in 2023.
@sagikazarmark I'd still prefer if we could re-enable this without new api. The existing AutomaticEnv
should already cover this case.
I used to get the desired behavior by pre-registering all keys like this:
// register all known config keys
flat, _ := flatten.Flatten(structs.Map(conf), "", flatten.DotStyle)
for _, v := range maps.Keys(flat) {
_ = viper.BindEnv(v)
}
What I'm totally failing to understand is why we're apparently triggering errors on the target struct side now (depending on the target struct) when the change should really modify what gets decoded, so on the input data side?
1723 uses a mapstructure version that should fix the multiple indirection problem.
I'd still like to look into what causes the same error with
interface{}
before enabling this feature by default again, but I don't think it's gonna happen this year.I may still add the runtime optional feature interface in 2023.
I ran into this problem yesterday and noticed that the fix in go-viper/mapstructure doesn't really go all the way. I propose the following change instead which seems to work for any input of pointers and interface nesting.
See: https://github.com/go-viper/mapstructure/compare/main...mazenharake:mapstructure:deepshave
I might be missing something but I tested it with the code below (warning: monstrosity incoming). Note: I did a local replace in the go.mod to point at the local go-viper/mapstructure package so dont trust the import path. Also, all tests in mapstructure and viper runs without errors.
if we can get #1723 in then I can create a pull request or you can just copy-paste it in since it is simple enough (haven't looked into your pull request routines yet).
Anyway, this should fix this issue.
How do you all feel about #1854?
I'd imagine it would become the default behavior at some point, so the experimental flag would become noop.
How do you all feel about #1854?
I'd imagine it would become the default behavior at some point, so the experimental flag would become noop.
This looks good, I'm glad that I will be able to remove the build tag from our pipelines/Dockerfiles/etc. It means our programs will behave correctly with plain go build
, which improves local development experience.
Just waiting for this to be released.
@cedws you can give it a try by upgrading to v1.20.0-alpha.1
Preflight Checklist
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:
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.