mapstructure将map[string]interface{}解码为包含自定义类型的结构体时出现错误。

huangapple go评论83阅读模式
英文:

mapstructure decode a map[string]interface{} into a struct containing custom type gives Error

问题

我的应用程序接收一个JSON字符串,将其解组为一个名为BiggerType的结构体,并使用mapstructure将结构体的字段Settings解码为类型MainTypeBiggerType需要支持多种类型的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,并使用你已经使用的自定义解码器来处理 SpecialTypemapstructure 在需要将 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

> * &#39;b&#39; expected a map, got &#39;string&#39;

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{} {
   &quot;A&quot;: map[string]interface{}{...},
   &quot;B&quot;: string(&quot;special_value&quot;),
}

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:&quot;other&quot;`
	Settings MainType `json:&quot;settings&quot;`
}

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.

huangapple
  • 本文由 发表于 2021年12月19日 22:58:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/70412529.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定