PAYATHON 2026

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 写入的字段必须是导出字段,也就是字段名的首字母必须大写。

备注:内容仅供参考。