Golang中将map[string]interface{}解码到包含自定义类型的结构体时遇到错误

Golang中将map[string]interface{}解码到包含自定义类型的结构体时遇到错误 我的应用程序接收一个JSON字符串,将其解组到结构体 BiggerType 中,然后将该结构体的字段 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 中,并保持所有值不变。请问有谁能与我分享任何解决方法或建议吗?

重现该问题的游乐场:Go Playground - The Go Programming Language

谢谢。


更多关于Golang中将map[string]interface{}解码到包含自定义类型的结构体时遇到错误的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

谢谢。你的解决方案有效。我认为自定义解码器/编码器会起作用。

更多关于Golang中将map[string]interface{}解码到包含自定义类型的结构体时遇到错误的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我尝试了一种变通方法,先将 biggerType.Settings 序列化为 []byte,然后再反序列化,这个方法可行,代码如下 Go Playground - The Go Programming Language 或者使用 json.Encoderjson.Decoder 可能更合理?

问题在于 mapstructure 默认无法处理 SpecialType 的自定义 UnmarshalJSON 方法。当 mapstructure 遇到 b 字段时,它看到一个字符串值,但 MainType.B 的类型是 SpecialType,而 SpecialType 期望通过 UnmarshalJSON 来解析。mapstructure 不知道这个自定义逻辑,因此尝试直接将字符串赋值给结构体字段,导致类型不匹配错误。

解决方案是使用 mapstructure 的解码钩子(DecodeHook)来告诉它如何将原始数据(例如字符串或 map[string]interface{})转换为 SpecialType。解码钩子允许你在解码过程中拦截特定类型的转换,并执行自定义逻辑。

以下是修改后的代码,添加了一个解码钩子来处理 SpecialType

package main

import (
	"encoding/json"
	"fmt"
	"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

	// 配置 mapstructure 解码器,添加自定义解码钩子
	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
			// 添加处理 SpecialType 的钩子
			specialTypeDecodeHook,
		),
		Result: &decodedMainType,
	})
	if err != nil {
		panic(err)
	}

	if err := decoder.Decode(biggerType.Settings); err != nil {
		panic(err)
	}

	// 验证解码结果
	fmt.Printf("Decoded MainType: %+v\n", decodedMainType)
	if decodedMainType.A.Aa != nil {
		fmt.Printf("A.Aa.Aaa.Value: %+v\n", decodedMainType.A.Aa.Aaa.Value)
	}
	fmt.Printf("B.Value: %+v\n", decodedMainType.B.Value)
}

// specialTypeDecodeHook 是一个 mapstructure 解码钩子,用于将原始数据转换为 SpecialType
func specialTypeDecodeHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
	// 检查目标类型是否为 SpecialType
	if t != reflect.TypeOf(SpecialType{}) {
		return data, nil
	}

	// 根据原始数据的类型,构建 SpecialType
	switch v := data.(type) {
	case string:
		// 如果原始数据是字符串,创建 SpecialTypeString 并包装到 SpecialType 中
		st := SpecialTypeString(v)
		return SpecialType{Value: st}, nil
	case map[string]interface{}:
		// 如果原始数据是 map,尝试将其转换为 SpecialTypeObject
		// 注意:这里需要手动处理 map 到 SpecialTypeObject 的转换
		// 为了简化,我们可以将其序列化为 JSON 再通过 json.Unmarshal 解析
		jsonData, err := json.Marshal(v)
		if err != nil {
			return nil, err
		}
		var sto SpecialTypeObject
		if err := json.Unmarshal(jsonData, &sto); err != nil {
			return nil, err
		}
		return SpecialType{Value: sto}, nil
	default:
		// 其他类型无法处理,返回错误
		return nil, fmt.Errorf("cannot decode %T into SpecialType", data)
	}
}

// 以下结构体定义保持不变
type BiggerType struct {
	Other    string                 `json:"other"`
	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)
}

type SpecialType struct {
	Value SpecialInterface
}

func (s *SpecialType) UnmarshalJSON(data []byte) error {
	// 尝试先解析为字符串
	var str string
	if err := json.Unmarshal(data, &str); err == nil {
		s.Value = SpecialTypeString(str)
		return nil
	}

	// 尝试解析为对象
	var obj SpecialTypeObject
	if err := json.Unmarshal(data, &obj); err == nil {
		s.Value = obj
		return nil
	}

	return fmt.Errorf("cannot unmarshal into SpecialType")
}

关键点:

  1. 创建了一个 specialTypeDecodeHook 函数,它检查目标类型是否为 SpecialType。如果是,它根据原始数据的类型(字符串或 map[string]interface{})创建相应的 SpecialType 实例。
  2. 使用 mapstructure.NewDecoder 创建一个自定义解码器,并将 specialTypeDecodeHook 添加到解码钩子链中。
  3. 在钩子函数中,对于字符串数据,我们创建 SpecialTypeString 并包装到 SpecialType 中;对于 map 数据,我们将其序列化为 JSON,然后反序列化到 SpecialTypeObject,再包装到 SpecialType 中。

这样,mapstructure 在解码过程中遇到 SpecialType 字段时,会使用我们的自定义钩子进行转换,从而正确处理字符串和对象两种形式。

回到顶部