Go 中将复杂 JSON 的 outputContexts 绑定到 struct
结论
不必先将 JSON 解析成 map[string]interface{}。定义与目标字段对应的嵌套 struct,再使用 json.Unmarshal 解析即可。
outputContexts 是 queryResult 中的数组,因此对应的字段类型应为 []OutputContext。如果只关心这部分数据,struct 无须覆盖整个 JSON。encoding/json 会自动忽略未定义的字段。
Struct 定义
type WebhookResponse struct {
QueryResult QueryResult `json:"queryResult"`
}
type QueryResult struct {
OutputContexts []OutputContext `json:"outputContexts"`
}
type OutputContext struct {
Name string `json:"name"`
LifespanCount int `json:"lifespanCount"`
Parameters ContextParameters `json:"parameters"`
}
type ContextParameters struct {
Name []string `json:"name"`
NameOriginal []string `json:"name.original"`
PhoneNumber string `json:"phone-number"`
PhoneNumberOriginal string `json:"phone-number.original"`
}
JSON 键 name.original 和 phone-number 中含有点号或连字符,无法直接用作 Go 字段名,但可以通过 struct tag 准确映射。
解析并提取指定 Context
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
)
type WebhookResponse struct {
QueryResult QueryResult `json:"queryResult"`
}
type QueryResult struct {
OutputContexts []OutputContext `json:"outputContexts"`
}
type OutputContext struct {
Name string `json:"name"`
LifespanCount int `json:"lifespanCount"`
Parameters ContextParameters `json:"parameters"`
}
type ContextParameters struct {
Name []string `json:"name"`
NameOriginal []string `json:"name.original"`
PhoneNumber string `json:"phone-number"`
PhoneNumberOriginal string `json:"phone-number.original"`
}
func main() {
// b 是 webhook 返回的 JSON 字节数据。
var response WebhookResponse
if err := json.Unmarshal(b, &response); err != nil {
log.Fatalf("解析 webhook JSON 失败: %v", err)
}
for _, context := range response.QueryResult.OutputContexts {
if strings.HasSuffix(context.Name, "/contexts/awaiting_name") {
fmt.Printf("context name: %s\n", context.Name)
fmt.Printf("lifespanCount: %d\n", context.LifespanCount)
fmt.Printf("name: %v\n", context.Parameters.Name)
fmt.Printf("name.original: %v\n", context.Parameters.NameOriginal)
fmt.Printf("phone-number: %s\n", context.Parameters.PhoneNumber)
fmt.Printf(
"phone-number.original: %s\n",
context.Parameters.PhoneNumberOriginal,
)
break
}
}
}
这里按完整路径的后缀 /contexts/awaiting_name 匹配,可以避免将 awaiting_name_confirm 误判为目标 context。
如果能确定目标始终位于数组的第一个元素,也可以直接读取:
contexts := response.QueryResult.OutputContexts
if len(contexts) == 0 {
log.Println("响应中没有 outputContexts")
return
}
context := contexts[0]
fmt.Printf("%+v\n", context)
不过,依赖数组顺序通常不够稳妥,按 context 名称查找更可靠。
为什么原来的代码没有找到数据
原代码检查的是:
if k == "queryResultmap"
实际的 JSON 键名则是:
"queryResult"
此外,outputContexts 不是顶层字段,其访问层级为:
queryResult
└── outputContexts
如果仍然使用 map[string]interface{},需要进行多次类型断言:
var value map[string]interface{}
if err := json.Unmarshal(b, &value); err != nil {
log.Fatal(err)
}
queryResult, ok := value["queryResult"].(map[string]interface{})
if !ok {
log.Fatal("queryResult 不是 JSON object")
}
outputContexts, ok := queryResult["outputContexts"].([]interface{})
if !ok {
log.Fatal("outputContexts 不是 JSON array")
}
fmt.Printf("%v\n", outputContexts)
这种写法同样可以工作,但字段越多,需要处理的类型断言就越多,也更容易出现运行时错误。数据结构相对固定时,使用 struct 更合适。
参数类型不固定时的处理方式
如果不同 context 的 parameters 中可能出现很多未知字段,或者同一字段的类型并不稳定,可以将其定义为 map[string]interface{}:
type OutputContext struct {
Name string `json:"name"`
LifespanCount int `json:"lifespanCount"`
Parameters map[string]interface{} `json:"parameters"`
}
读取字段时仍需进行类型断言:
for _, context := range response.QueryResult.OutputContexts {
if !strings.HasSuffix(context.Name, "/contexts/awaiting_name") {
continue
}
phoneNumber, ok := context.Parameters["phone-number"].(string)
if !ok {
log.Println("phone-number 不存在或不是字符串")
continue
}
namesValue, ok := context.Parameters["name"].([]interface{})
if !ok {
log.Println("name 不存在或不是数组")
continue
}
fmt.Println(phoneNumber)
fmt.Println(namesValue)
}
对于题目给出的 JSON,明确声明 ContextParameters struct 用起来更方便。只有当参数结构确实会动态变化时,才需要改用 map[string]interface{}。
注意事项
- 必须检查
json.Unmarshal返回的错误,不要直接忽略。 outputContexts是数组,访问其中的元素前应先检查长度。- JSON 缺少某个字段时不会导致解析失败,对应字段会保留 Go 类型的零值。例如,缺少
lifespanCount时,其值为0。 - 如果需要区分“字段不存在”和“字段存在且值为 0”,可以将字段定义为指针:
LifespanCount *int `json:"lifespanCount"`
- Go struct 中需要由
encoding/json写入的字段必须是导出字段,也就是字段名的首字母必须大写。
备注:内容仅供参考。
