使用 mapstructure 解析 json

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 使用 mapstructure 解析 json

背景


前几天群里的小伙伴问了一个这样的问题:



其实质就是在面对 value 类型不确定的情况下,怎么解析这个 json?


我下意识就想到了 [mapstructure](https://github.com/mitchellh/mapstructure) 这个库,它可以帮助我们类似 PHP 那样去处理弱类型的结构。

介绍



先来介绍一下 mapstructure 这个库主要用来做什么的吧,官网是这么介绍的:


mapstructure 是一个 Go 库,用于将通用映射值解码为结构,反之亦然,同时提供有用的错误处理。

该库在解码数据流(JSON、Gob 等)中的值时最为有用,因为在读取部分数据之前,您并不十分清楚底层数据的结构。因此,您可以读取 map[string]interface{} 并使用此库将其解码为适当的本地 Go 底层结构。


简单来说,它擅长解析一些我们并不十分清楚底层数据结构的数据流到我们定义的结构体中。


下面我们通过几个例子来简单介绍一下 mapstructure 怎么使用。


例子


普通形式


func normalDecode() {
 type Person struct {
  Name   string
  Age    int
  Emails []string
  Extra  map[string]string
 }
 // 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
 input := map[string]interface{}{
  "name":   "Tim",
  "age":    31,
  "emails": []string{"one@gmail.com", "two@gmail.com", "three@gmail.com"},
  "extra": map[string]string{
   "twitter": "Tim",
  },
 }
 var result Person
 err := mapstructure.Decode(input, &result)
 if err != nil {
  panic(err)
 }
 fmt.Printf("%#v\n", result)
}


输出:


main.Person{Name:"Tim", Age:31, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Tim"}}


这个方式应该是我们最经常使用的,非常简单的将 map[string]interface{} 映射到我们的结构体中。


在这里,我们并没有指定每个 fieldtag,让 mapstructure 自动去映射。


如果我们的 input 是一个 json 字符串,那么我们需要将 json 字符串解析为 map[string]interface{} 之后,再将其映射到我们的结构体中。


func jsonDecode() {
 var jsonStr = `{
 "name": "Tim",
 "age": 31,
 "gender": "male"
}`
 type Person struct {
  Name   string
  Age    int
  Gender string
 }
 m := make(map[string]interface{})
 err := json.Unmarshal([]byte(jsonStr), &m)
 if err != nil {
  panic(err)
 }
 var result Person
 err = mapstructure.Decode(m, &result)
 if err != nil {
  panic(err.Error())
 }
 fmt.Printf("%#v\n", result)
}


输出:


main.Person{Name:"Tim", Age:31, Gender:"male"}


嵌入式结构


mapstructure 允许我们压缩多个嵌入式结构,并通过 squash 标签进行处理。


func embeddedStructDecode() {
 // 使用 squash 标签允许压缩多个嵌入式结构。通过创建多种类型的复合结构并对其进行解码来演示此功能。
 type Family struct {
  LastName string
 }
 type Location struct {
  City string
 }
 type Person struct {
  Family    `mapstructure:",squash"`
  Location  `mapstructure:",squash"`
  FirstName string
 }
 input := map[string]interface{}{
  "FirstName": "Tim",
  "LastName":  "Liu",
  "City":      "China, Guangdong",
 }
 var result Person
 err := mapstructure.Decode(input, &result)
 if err != nil {
  panic(err)
 }
 fmt.Printf("%s %s, %s\n", result.FirstName, result.LastName, result.City)
}


输出:


Tim Liu, China, Guangdong


在这个例子中, Person 里面有着 LocationFamily 的嵌入式结构体,通过 squash 标签进行压缩,从而达到平铺的作用。


元数据


func metadataDecode() {
 type Person struct {
  Name   string
  Age    int
  Gender string
 }
 // 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
 input := map[string]interface{}{
  "name":  "Tim",
  "age":   31,
  "email": "one@gmail.com",
 }
 // 对于元数据,我们制作了一个更高级的 DecoderConfig,以便我们可以更细致地配置所使用的解码器。在这种情况下,我们只是告诉解码器我们想要跟踪元数据。
 var md mapstructure.Metadata
 var result Person
 config := &mapstructure.DecoderConfig{
  Metadata: &md,
  Result:   &result,
 }
 decoder, err := mapstructure.NewDecoder(config)
 if err != nil {
  panic(err)
 }
 if err = decoder.Decode(input); err != nil {
  panic(err)
 }
 fmt.Printf("value: %#v, keys: %#v, Unused keys: %#v, Unset keys: %#v\n", result, md.Keys, md.Unused, md.Unset)
}


输出:


value: main.Person{Name:"Tim", Age:31, Gender:""}, keys: []string{"Name", "Age"}, Unused keys: []string{"email"}, Unset keys: []string{"Gender"}


从这个例子我们可以看出,使用 Metadata 可以记录我们结构体以及 map[string]interface{} 的差异,相同的部分会正确映射到对应的字段中,而差异则使用了 UnusedUnset 来表达。


  • Unused:map 中有着结构体所没有的字段。
  • Unset:结构体中有着 map 中所没有的字段。


避免空值的映射


这里的使用其实和内置的 json 库使用方式是一样的,都是借助 omitempty 标签来解决。


func omitemptyDecode() {
 // 添加 omitempty 注释以避免空值的映射键
 type Family struct {
  LastName string
 }
 type Location struct {
  City string
 }
 type Person struct {
  *Family   `mapstructure:",omitempty"`
  *Location `mapstructure:",omitempty"`
  Age       int
  FirstName string
 }
 result := &map[string]interface{}{}
 input := Person{FirstName: "Somebody"}
 err := mapstructure.Decode(input, &result)
 if err != nil {
  panic(err)
 }
 fmt.Printf("%+v\n", result)
}


输出:


&map[Age:0 FirstName:Somebody]


这里我们可以看到 *Family*Location 都被设置了 omitempty,所以在解析过程中会忽略掉空值。而 Age 没有设置,并且 input 中没有对应的 value,所以在解析中使用对应类型的零值来表达,而 int 类型的零值就是 0


剩余字段


func remainDataDecode() {
 type Person struct {
  Name  string
  Age   int
  Other map[string]interface{} `mapstructure:",remain"`
 }
 input := map[string]interface{}{
  "name":   "Tim",
  "age":    31,
  "email":  "one@gmail.com",
  "gender": "male",
 }
 var result Person
 err := mapstructure.Decode(input, &result)
 if err != nil {
  panic(err)
 }
 fmt.Printf("%#v\n", result)
}


输出:


main.Person{Name:"Tim", Age:31, Other:map[string]interface {}{"email":"one@gmail.com", "gender":"male"}}


从代码可以看到 Other 字段被设置了 remain,这意味着 input 中没有正确映射的字段都会被放到 Other 中,从输出可以看到,emailgender 已经被正确的放到 Other 中了。



自定义标签


func tagDecode() {
 // 请注意,结构类型中定义的 mapstructure 标签可以指示将值映射到哪些字段。
 type Person struct {
  Name string `mapstructure:"person_name"`
  Age  int    `mapstructure:"person_age"`
 }
 input := map[string]interface{}{
  "person_name": "Tim",
  "person_age":  31,
 }
 var result Person
 err := mapstructure.Decode(input, &result)
 if err != nil {
  panic(err)
 }
 fmt.Printf("%#v\n", result)
}


输出:


main.Person{Name:"Tim", Age:31}


Person 结构中,我们将 person_nameperson_age 分别映射到 NameAge 中,从而达到在不破坏结构的基础上,去正确的解析。


弱类型解析


正如前面所说,mapstructure 提供了类似 PHP 解析弱类型结构的方法。


func weaklyTypedInputDecode() {
 type Person struct {
  Name   string
  Age    int
  Emails []string
 }
 // 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,由 PHP 等弱类型语言生成。
 input := map[string]interface{}{
  "name":   123,  // number => string
  "age":    "31", // string => number
  "emails": map[string]interface{}{}, // empty map => empty array
 }
 var result Person
 config := &mapstructure.DecoderConfig{
  WeaklyTypedInput: true,
  Result:           &result,
 }
 decoder, err := mapstructure.NewDecoder(config)
 if err != nil {
  panic(err)
 }
 err = decoder.Decode(input)
 if err != nil {
  panic(err)
 }
 fmt.Printf("%#v\n", result)
}


输出:


main.Person{Name:"123", Age:31, Emails:[]string{}}


从代码可以看到,input 中的 nameagePerson 结构体中的 NameAge 类型不一致,而 email 更是离谱,一个字符串数组,一个是 map


但是我们通过自定义 DecoderConfig,将 WeaklyTypedInput 设置成 true 之后,mapstructure 很容易帮助我们解决这类弱类型的解析问题。


但是也不是所有问题都能解决,通过源码我们可以知道有如下限制:


//   - bools to string (true = "1", false = "0")
//   - numbers to string (base 10)
//   - bools to int/uint (true = 1, false = 0)
//   - strings to int/uint (base implied by prefix)
//   - int to bool (true if value != 0)
//   - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F,
//     FALSE, false, False. Anything else is an error)
//   - empty array = empty map and vice versa
//   - negative numbers to overflowed uint values (base 10)
//   - slice of maps to a merged map
//   - single values are converted to slices if required. Each
//     element is weakly decoded. For example: "4" can become []int{4}
//     if the target type is an int slice.


大家使用这种弱类型解析的时候也需要注意。

错误处理


mapstructure 错误提示非常的友好,下面我们来看看遇到错误时,它是怎么提示的。


func decodeErrorHandle() {
 type Person struct {
  Name   string
  Age    int
  Emails []string
  Extra  map[string]string
 }
 input := map[string]interface{}{
  "name":   123,
  "age":    "bad value",
  "emails": []int{1, 2, 3},
 }
 var result Person
 err := mapstructure.Decode(input, &result)
 if err != nil {
  fmt.Println(err.Error())
 }
}


输出:


5 error(s) decoding:
* 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value'
* 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1'
* 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2'
* 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3'
* 'Name' expected type 'string', got unconvertible type 'int', value: '123'


这里的错误提示会告诉我们每个字段,字段里的值应该需要怎么表达,我们可以通过这些错误提示,比较快的去修复问题。


总结


从上面这些例子看看到 mapstructure 的强大之处,很好的帮我们解决了实实在在的问题,也在节省我们的开发成本。

但是从源码来看,内部使用了大量的反射,这可能会对一些特殊场景带来性能隐患。所以大家在使用的时候,一定要充分考虑产品逻辑以及场景。


以下贴一小段删减过的源码:


// Decode decodes the given raw interface to the target pointer specified
// by the configuration.
func (d *Decoder) Decode(input interface{}) error {
 return d.decode("", input, reflect.ValueOf(d.config.Result).Elem())
}
// Decodes an unknown data type into a specific reflection value.
func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error {
 ....
 var err error
 outputKind := getKind(outVal)
 addMetaKey := true
 switch outputKind {
 case reflect.Bool:
  err = d.decodeBool(name, input, outVal)
 case reflect.Interface:
  err = d.decodeBasic(name, input, outVal)
 case reflect.String:
  err = d.decodeString(name, input, outVal)
 case reflect.Int:
  err = d.decodeInt(name, input, outVal)
 case reflect.Uint:
  err = d.decodeUint(name, input, outVal)
 case reflect.Float32:
  err = d.decodeFloat(name, input, outVal)
 case reflect.Struct:
  err = d.decodeStruct(name, input, outVal)
 case reflect.Map:
  err = d.decodeMap(name, input, outVal)
 case reflect.Ptr:
  addMetaKey, err = d.decodePtr(name, input, outVal)
 case reflect.Slice:
  err = d.decodeSlice(name, input, outVal)
 case reflect.Array:
  err = d.decodeArray(name, input, outVal)
 case reflect.Func:
  err = d.decodeFunc(name, input, outVal)
 default:
  // If we reached this point then we weren't able to decode it
  return fmt.Errorf("%s: unsupported type: %s", name, outputKind)
 }
 // If we reached here, then we successfully decoded SOMETHING, so
 // mark the key as used if we're tracking metainput.
 if addMetaKey && d.config.Metadata != nil && name != "" {
  d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
 }
 return err
}
相关文章
|
17天前
|
SQL 存储 JSON
SQL,解析 json
SQL,解析 json
44 8
|
2月前
|
存储 JSON API
淘系API接口(解析返回的json数据)商品详情数据解析助力开发者
——在成长的路上,我们都是同行者。这篇关于商品详情API接口的文章,希望能帮助到您。期待与您继续分享更多API接口的知识,请记得关注Anzexi58哦! 淘宝API接口(如淘宝开放平台提供的API)允许开发者获取淘宝商品的各种信息,包括商品详情。然而,需要注意的是,直接访问淘宝的商品数据API通常需要商家身份或开发者权限,并且需要遵循淘宝的API使用协议。
淘系API接口(解析返回的json数据)商品详情数据解析助力开发者
|
1月前
|
JSON API 数据格式
requests库中json参数与data参数使用方法的深入解析
选择 `data`或 `json`取决于你的具体需求,以及服务器端期望接收的数据格式。
135 2
|
1月前
|
JSON 前端开发 JavaScript
解析JSON文件
解析JSON文件
93 9
|
16天前
|
JSON JavaScript API
商品详情数据接口解析返回的JSON数据(API接口整套流程)
商品详情数据接口解析返回的JSON数据是API接口使用中的一个重要环节,它涉及从发送请求到接收并处理响应的整个流程。以下是一个完整的API接口使用流程,包括如何解析返回的JSON数据:
|
2月前
|
JSON Java API
在 Java 中解析 JSON ArrayList 的详细指南
【8月更文挑战第23天】
54 1
|
2月前
|
JSON 开发框架 JavaScript
【Azure Developer】使用.Net Core解析JSON的笔记
【Azure Developer】使用.Net Core解析JSON的笔记
|
1月前
|
存储 JSON API
Python编程:解析HTTP请求返回的JSON数据
使用Python处理HTTP请求和解析JSON数据既直接又高效。`requests`库的简洁性和强大功能使得发送请求、接收和解析响应变得异常简单。以上步骤和示例提供了一个基础的框架,可以根据你的具体需求进行调整和扩展。通过合适的异常处理,你的代码将更加健壮和可靠,为用户提供更加流畅的体验。
113 0
|
2月前
|
JSON API 数据格式
基于服务器响应的实时天气数据进行JSON解析的详细代码及其框架
【8月更文挑战第25天】这段资料介绍了一个使用Python从服务器获取实时天气数据并解析JSON格式数据的基本框架。主要分为三个部分:一是安装必要的`requests`库以发起HTTP请求获取数据,同时利用Python内置的`json`库处理JSON数据;二是提供了具体的代码实现,包括获取天气数据的`get_weather_data`函数和解析数据的`parse_weather_data`函数;三是对代码逻辑进行了详细说明,包括如何通过API获取数据以及如何解析这些数据来获取温度和天气描述等信息。用户需要根据实际使用的天气API调整代码中的API地址、参数和字段名称。
|
2月前
|
JSON 数据格式 索引
【Azure Developer】Azure Logic App 示例: 解析 Request Body 的 JSON 的表达式? triggerBody()?
【Azure Developer】Azure Logic App 示例: 解析 Request Body 的 JSON 的表达式? triggerBody()?

推荐镜像

更多