英文:
mapstructure decode a map[string]interface{} into a struct containing custom type gives Error
问题
我的应用程序接收一个JSON字符串,将其解组为一个名为BiggerType
的结构体,并使用mapstructure将结构体的字段Settings
解码为类型MainType
。BiggerType
需要支持多种类型的Settings
,因此必须声明为map[string]interface{}
。该应用程序在出现新类型的Settings
(即包含类型为SpecialType
的自定义字段的MainType
)之前一直正常工作。
下面是结构体和主要代码。运行代码会出现以下错误。
* 'b' expected a map, got 'string'
为了简洁起见,删除了一些代码。
package main
import (
...
"github.com/mitchellh/mapstructure"
)
const myJSON = `{
"settings": {
"a": {
"aa": {
"aaa": {
"sta": "special_object_A",
"stb": "special_object_B"
},
"aab": "bab"
},
"ab": true
},
"b": "special_string"
},
"other": "other"
}`
func main() {
var biggerType BiggerType
err := json.Unmarshal([]byte(myJSON), &biggerType)
if err != nil {
panic(err)
}
var decodedMainType MainType
if err := mapstructure.Decode(biggerType.Settings, &decodedMainType); err != nil {
panic(err)
}
}
type BiggerType struct {
Other string `json:"other"`
// Settings MainType `json:"settings"` 无法使用,因为它需要支持其他的"Settings"
Settings map[string]interface{} `json:"settings"`
}
type A struct {
Aa *AA `json:"aa"`
Ab *bool `json:"ab"`
}
type AA struct {
Aaa SpecialType `json:"aaa"`
Aab string `json:"aab"`
}
type MainType struct {
A A `json:"a"`
B SpecialType `json:"b"`
}
type SpecialTypeObject struct {
Sta string `json:"sta"`
Stb string `json:"stb"`
}
func (s SpecialTypeObject) InterfaceMethod() (string, error) {
return s.Sta + "+" + s.Stb, nil
}
type SpecialTypeString string
func (s SpecialTypeString) InterfaceMethod() (string, error) {
return string(s), nil
}
type SpecialInterface interface {
InterfaceMethod() (string, error)
}
// SpecialType SpecialTypeString | SpecialTypeObject
type SpecialType struct {
Value SpecialInterface
}
func (s *SpecialType) UnmarshalJSON(data []byte) error {
...
}
我的目标是能够将biggerType.Settings
解码为decodedMainType
,并保留所有的值。请问有人可以与我分享任何解决方法或建议吗?
可以使用以下链接复制问题:https://go.dev/play/p/G6mdnVoE2vZ
谢谢。
英文:
My app takes a JSON string, unmarshal into a struct BiggerType
and then, using mapstructure to decode the struct's field Settings
into a type MainType
. BiggerType
needs to support multiple types of Settings
and hence, is and has to be declared as map[string]interface{}
. The app used to be working fine until we have a new type of Settings
, i.e. MainType
containing some custom fields with the type SpecialType
.
The structs and the main codes are included below. Running the code gives the following error.
* 'b' expected a map, got 'string'
Some codes were removed for brevity
package main
import (
...
"github.com/mitchellh/mapstructure"
)
const myJSON = `{
"settings": {
"a": {
"aa": {
"aaa": {
"sta": "special_object_A",
"stb": "special_object_B"
},
"aab": "bab"
},
"ab": true
},
"b": "special_string"
},
"other": "other"
}`
func main() {
var biggerType BiggerType
err := json.Unmarshal([]byte(myJSON), &biggerType)
if err != nil {
panic(err)
}
var decodedMainType MainType
if err := mapstructure.Decode(biggerType.Settings, &decodedMainType); err != nil {
panic(err)
}
}
type BiggerType struct {
Other string `json:"other"`
// Settings MainType `json:"settings"` can't be used as it needs to support other "Settings"
Settings map[string]interface{} `json:"settings"`
}
type A struct {
Aa *AA `json:"aa"`
Ab *bool `json:"ab"`
}
type AA struct {
Aaa SpecialType `json:"aaa"`
Aab string `json:"aab"`
}
type MainType struct {
A A `json:"a"`
B SpecialType `json:"b"`
}
type SpecialTypeObject struct {
Sta string `json:"sta"`
Stb string `json:"stb"`
}
func (s SpecialTypeObject) InterfaceMethod() (string, error) {
return s.Sta + "+" + s.Stb, nil
}
type SpecialTypeString string
func (s SpecialTypeString) InterfaceMethod() (string, error) {
return string(s), nil
}
type SpecialInterface interface {
InterfaceMethod() (string, error)
}
// SpecialType SpecialTypeString | SpecialTypeObject
type SpecialType struct {
Value SpecialInterface
}
func (s *SpecialType) UnmarshalJSON(data []byte) error {
...
}
My goal is to be able to decode biggerType.Settings
into decodedMainType
with all the values intact. Can anyone please share with me any workaround or/and suggestions?
The playground to replicate the issue: https://go.dev/play/p/G6mdnVoE2vZ
Thank you.
答案1
得分: 4
我和Daniel Farrell的观点一致,但是可以修复现有的解决方案。
思路是利用mapstructure
的能力来自定义其Decoder
,该解码器可以接受一个在解码过程中在关键点调用的“hook”。有多个可用的钩子,它们可以组合成“链式”钩子。
由于有一个默认的钩子可以检查目标变量的类型是否实现了encoding.TextUnmarshaler
,我们可以调整你的(*SpecialType).UnmarshalJSON
方法,将其变为(*SpecialType).UnmarshalText
,然后使用它:
func (s *SpecialType) UnmarshalText(data []byte) error {
if len(data) > 0 && data[0] != '{' {
s.Value = SpecialTypeString(data)
return nil
}
var obj SpecialTypeObject
if err := json.Unmarshal(data, &obj); err != nil {
return fmt.Errorf("failed to unmarshal SpecialType: %s", err)
}
s.Value = obj
return nil
}
以及
var decodedMainType MainType
msd, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.TextUnmarshallerHookFunc(),
Result: &decodedMainType,
})
if err != nil {
return BiggerType{}, err
}
if err := msd.Decode(biggerType.Settings); err != nil {
return BiggerType{}, err
}
完整的解决方案请参考:https://go.dev/play/p/jaCD9FdSECz。
但是正如Daniel所暗示的,你似乎因为mapstructure
承诺提供了一个简单的解决方案来处理看似复杂的问题而使用它。
实际上,encoding/json
似乎已经提供了适用于你的用例的默认功能,只需使特定类型实现json.Unmarshaler
接口,所以我建议在这种情况下放弃使用mapstructure
:它是一个有用的包,但现在你似乎正在绕过它实现的框架的规则。
英文:
I'm with Daniel Farrell on this, but it's possible to fix the existing solution.
The idea is to make use of the mapstructure
's ability to customize its Decoder
which can take a "hook" which is called at key points during the decoding process. There are multiple available hooks and they can be combined into a "chain-style" hook.
Since there's a stock hook which is able to check whether the target variable's type implements encoding.TextUnmarshaler
, we can tweak your (*SpecialType).UnmarshalJSON
to become (*SpecialType).UnmarshalText
and then use that:
func (s *SpecialType) UnmarshalText(data []byte) error {
if len(data) > 0 && data[0] != '{' {
s.Value = SpecialTypeString(data)
return nil
}
var obj SpecialTypeObject
if err := json.Unmarshal(data, &obj); err != nil {
return fmt.Errorf("failed to unmarshal SpecialType: %s", err)
}
s.Value = obj
return nil
}
and
var decodedMainType MainType
msd, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.TextUnmarshallerHookFunc(),
Result: &decodedMainType,
})
if err != nil {
return BiggerType{}, err
}
if err := msd.Decode(biggerType.Settings); err != nil {
return BiggerType{}, err
}
The complete solution: <https://go.dev/play/p/jaCD9FdSECz>.
But then again — as Daniel hinted at, you appear to have fallen for using mapstructure
because it promised to be a no-brainer solution for a problem which appeared to be complicated.
In fact, encoding/json
appears to have stock facilities to cover your use case — exactly through making certain types implement json.Unmarshaler
, so I'd try to ditch mapstructure
for this case: it's a useful package, but now you appear to be working around the rules of the framework it implements.
答案2
得分: 3
为什么它不起作用
* 'b' expected a map, got 'string'
在 JSON 中,settings.b
是一个字符串类型。你已经将其解码为一个 BiggerType
,其中 settings
是一个 map[string]interface{}
。标准库提供的 JSON 解码过程会生成一个类似下面的 map:
map[string]interface{} {
"A": map[string]interface{}{...},
"B": string("special_value"),
}
JSON 解码已经完成。程序中不再进行其他 JSON 解码。
然后,你尝试使用 mapstructure
将你的 settings
map 解码为一个 MainType
,其中 B
的类型是 SettingsType
。因此,mapstructure.Decode
忠实地遍历你的 BiggerType
,试图将其转换为 MainType
,但遇到了问题。当它到达 B
时,输入是一个字符串,而输出是一个 SettingsType
。
你为 settings 类型编写了一个 UnmarshalJSON
函数,但是 B
的值并没有从 JSON 中解组,它已经是一个字符串了。Decode 不知道如何将一个字符串转换为 SpecialType
(它说它期望一个 map,但这有点误导,因为它实际上检查了 map、struct、slice 和数组类型)。
重新表述你面临的问题
那么让我们更具体地确定一下错误:你有一个 map[string]interface{}
,其中包含一个字符串值,mapstructure.Decode
尝试将其解码为一个具有结构值的结构字段。
重新表述目标
你在这里展示的一切都是你在解决方案中遇到的问题。你的根本问题是什么?你有一个 BiggerType.Settings
中的 interface{}
,它可能是一个字符串,也可能是一个 map[string]interface{}
,你想将其转换为一个 SpecialType
,它可以保存一个 SpecialTypeString
或一个 SpecialTypeObj
。
不过,我无法完全理解你的问题,因为你一直在谈论 MainType
可能是不同类型,但是你程序中的所有不同类型似乎都在 SpecialType
下。
如何解决
在这里,似乎明显的解决方案是放弃 mapstructure
,并使用你已经使用的自定义解码器来处理 SpecialType
。mapstructure
在需要将 JSON 解码为内省某些属性,然后根据其他属性做出决策的情况下非常有用。但是你并没有使用 mapstructure
来做任何类似的事情;最接近的是你的自定义解码器。如果自定义解码器可以完成工作,你就不需要 mapstructure
。
你代码中的 SpecialType.UnmarshalJSON
已经是一个示例,说明了如何将未知值包装在已知类型中,并使用 switch
语句处理未知值类型。
所以,只需放弃 mapstructure
,将 BiggerType
重新定义为:
type BiggerType struct {
Other string `json:"other"`
Settings MainType `json:"settings"`
}
然后让已经编写好的 UnmarshalJSON
处理 MainType.B
。
如果不是 SpecialType
需要这种逻辑,而是 MainType
,那么对 SpecialType
做与对 MainType
的处理相同的操作。
如果你想看到一个使用 mapstructure
的解决方案,请提供多个 JSON 输入示例,以便我可以看到它们的区别以及为什么可能需要 mapstructure
。
英文:
Why it doesn't work
> * 'b' expected a map, got 'string'
In the JSON, settings.b
is a string type. You've decoded it into a BiggerType
in which settings is a map[string]interface{}
. The standard library provided JSON Unmarshalling process results in a map like:
map[string]interface{} {
"A": map[string]interface{}{...},
"B": string("special_value"),
}
JSON decoding is now done. No more Json decoding happens in the program.
You then attempt to use mapstructure
to Decode your settings
map
into a MainType
, where B
is of type SettingsType
. So mapstructure.Decode
dutifully iterates over your BiggerType
and attempts to turn it into a MainType
, but hits a snag. when it gets to B
, the input is a string, and the output is a SettingsType
.
You wrote an UnmarshalJSON
function for settings type, but B
's value is not being unmarshalled from JSON - it's already a string. Decode doesn't know how to turn a string
into a SpecialType
( it says it expected a map
but that's a little misleading as it actually checks for map, struct, slice, and array types)
Rephrasing the problem you're facing
So then let's identify the error a little more specifically: You have. map[string]interface{}
with a string value, mapstructure.Decode
tries to decode it into a struct field with a Struct value.
Rephrasing the goal
Everything you've shown here is issues you're facing with your solution. What is your underlying problem? You have an interface{}
in BiggerType.Settings
that might be a string
and might be a map[string]interface{}
, and you want to turn it into a SpecialType
that holds either a SpecialTypeString
or a SpecialTypeObj
I can't entirely square that with your question though, where you keep talking about MainType
being possibly different types, but all the different types in your program seem to be under SpecialType
.
How to Solve
It seems like the obvious solution here is to ditch mapstructure
and use the method you already use for SpecialType
with a custom unmarshaller. mapstructure
is useful for cases when you need to unmarshal JSON to introspect some attributes and then make a decision about how to handle other attributes. You're not doing anything like that with mapstructure
though; the closest thing is your custom unmarshaller. If a custom Unmarshaller can do the job, you don't need mapstructure
.
SpecialType.UnmarshalJSON
in your code is already an example of how you could wrap an unknown value in a known type and use a switch
statement to handle unknown value types.
So just ditch mapstructure
, redefine BiggerType
as:
type BiggerType struct {
Other string `json:"other"`
Settings MainType `json:"settings"`
}
And let the already-written UnmarshalJSON
handle MainType.B
.
If it's not SpecialType
that needs this logic, but MainType
, then do the same thing with SpecialType
that you did to MainType
.
https://go.dev/play/p/DW8N8t3Lr0l
If you want to see a solution with mapstructure
, please provide more than one example of the JSON input so I can see how they differ and why mapstructure
might be required.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论