Skip to content

feat(net/ghttp,net/goai): support JSON array request body by the in:"body" field tag - #4898

Open
LanceAdd wants to merge 2 commits into
gogf:masterfrom
LanceAdd:fix/array_body
Open

LanceAdd wants to merge 2 commits into
gogf:masterfrom
LanceAdd:fix/array_body

Conversation

@LanceAdd

@LanceAdd LanceAdd commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

背景

对 #4618 所要解决的问题的另一种实现方式

规范路由(标准路由)的请求结构体只能接收 JSON 对象体:parseBody 把 body 塞进 map[string]any,顶层数组必然进不来。想批量提交 [{"role":"user"},{"role":"assistant"}] 这种数组体,今天两条路都不通:

场景 今天的行为
Content-Type: application/json + 数组体 数组解不进 map,报 CodeInvalidParameter
非 JSON content-type + 数组体 落到 gstr.Parse 兜底:字段全零,而且会切出脏参数

第二条尤其坑:gstr.Parse 按 & 和 = 切分,所以 [{"url":"https://x.com?a=1&b=2"}] 会产出一个叫 b 的假请求参数——请求成功了,但 b 从哪来的没人说得清。

文档侧同样表达不了:goai 只能生成对象形态的请求体,生成不出 type: array + items: $ref。

为什么不走 type:"array" + 「第一个切片字段」

社区 PR #4618 用 g.Meta 的 type:"array" 标签解决,把数组绑到请求结构体的第一个切片字段。这个做法有三个问题,最终没有采用:

一、靠字段声明顺序猜接收方,会静默走错。 「取第一个切片字段」成立,仅仅因为 Items 恰好写在 Tags 前面。把 Tags 往上一行挪,行为就变了;用户往前面插一个切片字段,接收方就悄悄换了——不报错、不打日志。

二、type 这个 tag 名已经有主。 字段级的 type:"file" 是既有的文件上传约定(Files []*ghttp.UploadFile 配 type:"file")。type:"array" 表示「请求体形状」,同一个词两套语义;而且上传字段本身也是切片,照样会被「第一个切片字段」的规则选中。

三、带了一个未声明的破坏性变更。 #4618 让非 JSON content-type 下的数组体,从「200 + 字段全零」变成参数错误。方向是对的(静默给零值更坑),但 PR 描述里没提。

改动

in:"body" —— 复用 GF 已有的「参数来源」标签 in,让字段自己声明它接收整个请求体:

type BatchChatReq struct {
    g.Meta   `mime:"application/json" method:"post" path:"/batch/chat" summary:"批量聊天接口"`
    Messages []ChatMessage `json:"messages" dc:"消息列表" in:"body"`
}

客户端直接提交 [{"role":"user"},{"role":"assistant"}],绑定到 Messages;结构体里的其它字段(路径参数、query 等)照旧合并,互不干扰。

in 不是新标签:gtag.In 本来就存在,goai 里已经实现 path / query / header / cookie 四个取值,文档里教的写法就是 in:"header" 指定参数位置。这里只是把 body 补进同一套语义。

范围:in:"body" 只能写在切片字段上,指向切片的指针也算;其它类型注册期直接报错,并且按类型给出路——定长数组提示换成切片 + 长度校验,结构体 / map / interface{} 提示「对象体由请求结构体的普通字段接收,不需要这个标签」。

另外,这个标签只对严格路由(func(ctx, req) (res, err))有意义——func(*ghttp.Request) 那种 handler 没有请求结构体,标签无从生效。

两条边界是有意划的:

  • 定长数组([N]T)不收。 实践中定长都用切片加长度校验表达(v:"length:N"),框架不必为此再支持一种形态;而且收数组就得连带修 gconv 的数组转换——切片转定长数组这条路本来就不通(实测请求会以 reflect.MakeSlice of non-slice type 失败),那是与这个特性无关的既有缺陷,不宜顺路拖进来。
  • 对象体不需要这个标签。 严格路由的请求结构体本身就是对象体:提交 {"a":1},里面的键会当请求参数用、绑到同名字段上,「整个对象体」本来就有常规做法。所以这不是「对象体还没做」,是不需要;真要做的其实是另一类需求——对象体原样透传(比如存原始 JSON、做签名校验),那个留给后面。

为什么叫 body,而不是 array_json_body 之类

最容易想到的质疑是:只支持 JSON 数组,叫 body 是不是太泛、会误导?不泛,四个理由:

  • in 是「参数位置」枚举,取值都应该是位置词。 现有 path / query / header / cookie 全是「这个参数从哪来」,不带形状和格式;body 正是这一列里缺的第五个位置。叫 array_json_body 就把「位置 + 形状 + 格式」三样塞进同一个值,以后支持对象透传、表单体,难道叫 object_json_body、form_body?名字只会越起越多。
  • 形状已经写在字段类型里。 Messages []ChatMessage 一眼就知道是数组、元素是什么;标签再说一遍是重复,而且类型不匹配时注册期就会报错,不靠名字提醒。
  • body 是行业原词。 Swagger/OpenAPI 2.0 的参数就写 in: body,表示「整个请求体就是这个参数」,它收对象也收数组,名字里从来没有形状。GF 现有四个取值也是照这套词汇来的。
  • 风格要一致。 GF 的 in 取值都是一个全小写单词;array_json_body 是 snake_case,array-body 的连字符又和 valid 规则名(max-length)那套风格撞车,读起来像规则而不是位置。

真要做「对象体原样透传」那天,in:"body" 配上 struct / map 字段就够,一个词不加、一行不删;直译名则要再起名或废弃老的。至于「会不会让人误解成什么都收」:写错类型是注册期报错、服务起不来,请求体和标签不匹配(对象体 / 表单体 / multipart)是明确报 CodeInvalidParameter,都不会静默。

实现

net/ghttp:注册期识别、校验、把名字准备好

handlerFuncInfo 加两个字段:ReqBodyFieldName(字段名,请求期绑数组用)和 ReqBodyFieldTagName(字段的 tag 名,请求期删同名参数用)。checkAndCreateFuncInfo 在已有的 ReqStructFields 之后扫一遍:

  • 命中 0 个:一切照旧;
  • 命中 1 个:校验类型是切片,记下两个名字;
  • 命中多个、或类型不对:直接报错(定长数组、结构体 / map 会带上对应的改法提示,见上面的「范围」)。

错误在注册 handler 的时候就会报出来:checkAndCreateFuncInfo 的错误在 BindHandler / group.Bind / 对象绑定三条路上都是 Logger().Fatal(...),服务直接起不来,而不是等到请求期才发现绑不上。多写一个 in:"body"、写到 string 字段上、写到定长数组上,都会在启动时明确告知。

两个名字在注册期解析一次,请求期只读:绑数组体用字段名(数组体就写在字段名这个键上),删同名参数用 tag 名。这样既省掉每请求一次字段遍历,也不会出现「绑定用注册期的结果、删除按请求期重算」两套结果(标签里写了变量时,两边会不一样)。

存的是字段名而不是 reflect.StructField.Index:ReqStructFields 由 RecursiveOptionEmbedded 产出,内嵌结构体里的字段,它的 Index 是相对那个内嵌结构体算的,FieldByIndex 会取错;存名字交给 gconv 按名字匹配,内嵌字段的「提升」天然就是对的。

net/ghttp:解析(parseBody)

parseBody 加一段数组检测,位置在原有 JSON 处理之前:

if r.isArrayRequestBodyExpected() && body[0] == '[' && body[len(body)-1] == ']' {
    var array []any
    if err := json.UnmarshalUseNumber(body, &array); err == nil {
        r.bodyArray = array
        return
    } else if strictJsonContentType {
        r.SetError(gerror.WrapCode(gcode.CodeInvalidParameter, err, "Parse JSON body failed"))
        return
    }
    // 不是合法 JSON 数组,回落到下面的默认参数解码
}

「是不是数组接口」由 isArrayRequestBodyExpected() 判断,读的是当前路由 handler 上注册期缓存的字段名——这就是由声明决定的开关:没写标签的接口一行代码都不受影响,不用把全框架的解析一起放松。

检测不看 content-type(和对象体在非 JSON content-type 下的宽松处理一致):只要以 [ 开头、] 结尾就试着解析,所以 text/plain、表单类型提交过来的数组体都能收。解析失败时再看 content-type:JSON 体报文法错误,其它体回落到原来的参数解码。

两个容易忽略的点:数组分支必须判在 bodyMap 默认解码之前(否则数组体先被 gstr.Parse 变成一堆脏参数);parseBody 开头要重置 bodyMap / bodyArray,否则中间件改 body 后调 ReloadParam 会拿到上一次的陈旧结果(数组体和对象体互相残留)。

表单解码之前也要先认数组。 GetRequestMap() 的顺序是 parseForm() → parseBody(),表单类型的请求体会先被切成参数,等 parseBody 认出数组已经晚了。实测:application/x-www-form-urlencoded 提交 [{"name":"https://x.com?a=1&id=777&b=2"}],数组绑对了,但 URL 里的 &id=777 也变成了参数 id,把结构体里的同名字段顶成了 777。

修法是在 parseForm() 的非 multipart 分支开头先问一句「这个接口是不是数组接口」(isArrayRequestBodyExpected()),是的话直接试解析数组、命中就清掉 formMap 返回,不再走标准库的表单解码。好处有两个:数组体不会被切碎;ReloadParam 之后残留的 formMap 也顺手清掉。text/plain 那种本来就不走表单分支的请求一直是干净的,可以当对照。

net/ghttp:绑定(doGetRequestStruct)

三件事:

一、先删掉跟字段 tag 同名的请求参数。 数组体写进 data[字段名],而 gconv 找字段的顺序是「tag 名精确匹配 → 字段名精确匹配 → 模糊匹配」——tag 名排在字段名前面,所以只要请求参数里有一个正好等于 tag 名的键(json:"messages" 对应 ?messages=...),它就会先被绑上去,把数组体顶掉。没有这一句时实测:?ids=99 加数组体 [1,2,3],拿到的是 [99];参数是标量、字段是结构体切片时更糟,gconv 转换直接 panic(500)。

删的范围只限「正好等于 tag 名」这一个键,不做模糊匹配。最初写的是「忽略大小写和 -_[] 之类的符号」的模糊比较,会误伤兄弟字段:结构体里同时有 Items []int(配 json:"items" in:"body")和 ItemS string(配 json:"item_s")时,?item_s=hello 的 hello 会被删掉——item_s 去掉符号正好等于 Items。而且模糊匹配根本抢不走数组体:数组体写在字段名的键上,精确匹配到它,就轮不到模糊匹配了。所以只删 tag 名这一个键,够用,也不误伤。

二、分别处理数组体、非数组体、没有 body 三种情况。

  • 有数组体:写进 data[字段名],数组体优先;
  • 对象体、表单体、multipart:明确报 CodeInvalidParameter(不静默留空切片)。multipart 的解析结果在 MultipartForm 里、不在 bodyMap 里,所以要单独判一下——否则它会被当成「没有 body」放过去,字段是 nil、multipart 里的字段却当参数合了进来。这样「普通字段的」「只有文件的」「空表单」三种 multipart 形态都会被拒,真正没有 body 的请求照旧放行;
  • 完全没有 body:显式写 data[字段名] = nil。一句 nil 就能挡住所有想填这个字段的参数:gconv 匹配字段的顺序是「tag 名 → 字段名 → 模糊」,nil 在字段名这一档被精确命中,之后的模糊匹配根本不会跑;字段因此稳定是零值,[]struct 字段收到标量参数时原来会 panic(500)也不会再发生。[] 与无 body 的区别由此保留:前者是空切片,后者是 nil。

三、gconv.Struct 照常转换,其它字段(路径参数、query、header/cookie、默认值)完全不受影响。

net/goai:文档侧

五处改动,缺一处都不行:

一、ParameterInBody 常量 + 参数循环放行。 parameter.In 是从 tag 映射进去的,newParameterRefWithStructMethod 的白名单里没有 body,会落到 invalid tag value "body" for In 的报错;而 oai.Add 的错误在 initOpenApi 里是 Logger().Fatalf(...)。不加这个 case,不是文档难看,是服务起不来。

二、请求体 schema 改用该字段的 schema。 必须绕开原来的 getRequestSchemaRef:它返回指向请求结构体组件的 $ref,那个组件是 {type: object, properties: {messages: [...]}}——Swagger 上会显示成对象 {"messages":[...]},正是要避免的包装形态。newSchemaRefWithGolangType 对切片直接产出 {type: array, items: {$ref: ...}},并把元素类型注册进 Components.Schemas,不需要另写数组 schema 生成器。字段上的 dc 顺手拿来做 requestBody.description(否则它会凭空消失:该字段既不产出 parameter,也不在请求体 schema 里)。

三、生成数组 schema 前先解引用指针。 *[]ChatMessage 这种字段,newSchemaRefWithGolangType 原来会先剥出 array,再对 Elem() 递归——于是文档里变成数组套数组,而运行期是正常的一维数组。加一次解引用后,*[]T 和 []T 的文档一致。

四、removeOperationDuplicatedProperties 补 nil 守卫。 这个函数在 addPath 里无条件调用,会直接解引用 Schema.Value.Properties;而数组 schema 的 Properties 就是 nil(属性表是对象才有的)。不补守卫,带路径参数的数组接口一生成文档就 panic。顺带也挡住了 Schema.Value == nil——原来的 Value != nil 守卫在它后面才出现,是既有隐患。

五、body 字段和 HTTP 注册用同一套扫描范围。 HTTP 注册期扫 ReqStructFields 用的是 RecursiveOptionEmbedded,而文档侧原来是在参数列表(RecursiveOptionEmbeddedNoTag)里顺手找 body 字段——两者对带标签的内嵌结构体结论不同:把 body 字段放在基类里、用带标签的方式内嵌进来(比如基类字段上写 json:"base")时,运行期能正常收到数组(实测通过),文档却给出指向请求结构体组件的 $ref,又变回要避免的包裹对象。修法是给 body 字段单独扫一份 RecursiveOptionEmbedded 的字段列表,参数列表的扫描方式保持不变(否则会顺带改掉其它参数的文档行为)。

行为等价性

下面的「普通接口」指没声明 in:"body" 的接口。

场景 今天 改后 是否一致
普通接口 + JSON content-type + 数组体 报 CodeInvalidParameter 同一分支同一错误,文案不变 一致
普通接口 + 非 JSON content-type + 数组体 200,gstr.Parse 兜底 该分支未改动 一致
其它 in 取值(path/query/header/cookie) — 不变 一致
in:"body" 字段 标签不可用(配了 OpenAPI 的接口启动即失败) 正常工作 新增能力
[] 空数组 —(数组体根本进不来) 空切片,非 nil 新增能力
无 body — nil 切片,不报错 新增能力
对象体、表单体、multipart 发给数组接口 — CodeInvalidParameter,不静默留空切片 新增能力
与字段 tag 同名的参数(含大小写/符号变体) —(标签本来不可用) 该字段忽略它:有数组体时用请求体的值,没有 body 时保持零值 新增能力
表单类型(urlencoded)的数组体 — 照收,且不再被表单解码切出脏参数 新增能力
内嵌结构体里的 body 字段 — 运行期与文档都是数组体(含带标签的内嵌) 新增能力
定长数组字段 —(标签本来不可用) 注册期报错,提示改用切片 + 长度校验 新增能力
结构体 / map / interface{} 字段 —(标签本来不可用) 注册期报错,提示对象体由请求结构体普通字段接收 新增能力

失败模式都是显式的,没有静默降级:

  • in:"body" 写多了、写到非切片字段上 → 注册期报错,进程起不来;
  • 对象体/表单体发给数组接口 → 请求期 CodeInvalidParameter;
  • 畸形 JSON 数组体 → 沿用原有的 Parse JSON body failed。

另外提醒一句:畸形 multipart 体(boundary 不对)仍然是框架既有的 panic → 500,任何 multipart 接口都一样,与本次改动无关。

钉住这些行为的用例:

  • Test_Request_JsonArrayBody(正常路径;另含非 JSON content-type 也能收)
  • Test_Request_JsonArrayBody_BodyTakesPrecedenceOverQuery(同名参数顶不掉数组体)
  • Test_Request_JsonArrayBody_Pointer(指向切片的指针字段)
  • Test_Request_JsonArrayBody_WithPathParam(路径参数与数组体共存)
  • Test_Request_JsonArrayBody_EmptyAndAbsent([] 是空切片、无 body 是 nil;无 body 时同名及大小写/符号变体参数都填不进来,标量参数也不再 panic)
  • Test_Request_JsonArrayBody_FormContentTypeNoPollution(表单类型的数组体照收且不切出脏参数;普通表单体仍报参数错误)
  • Test_Request_JsonArrayBody_MultipartRejected(普通字段、只有文件、空表单三种 multipart 形态都报参数错误)
  • Test_Request_JsonArrayBody_Nested(元素含嵌套结构体与嵌套切片)
  • Test_Request_JsonArrayBody_ObjectBodyRejected(对象体必须报参数错误)
  • Test_Request_JsonArrayBody_PlainEndpointUnchanged(普通接口的数组体仍是原错误、普通对象体照常工作)
  • Test_HandlerFuncInfo_CheckAndCreateReqBodyField(注册期校验:无标签 / 切片 / 指向切片的指针 / 没有 tag 名时回落字段名 / 非切片 / 定长数组 / 结构体字段 / 多标签;定长数组与结构体两种报错文案里的出路提示也一并钉住)
  • TestOpenApiV3_ArrayRequestBody(数组 schema、元素注册、字段不产出 parameter、请求体带 path 参数时不被删、dc 进 description、*[]T 是一维数组、带标签/无标签内嵌结构体里的 body 字段都是数组体)

验证方式:Linux 容器(golang:1.25)里跑 net/ghttp 里请求与路由相关的那批用例(-run "Test_Request|Test_Params|Test_Router|Test_RouteServe|Test_Middleware|Test_Hook|Test_Group|Test_OpenApi")、net/goai、util/gconv、encoding/gjson,全绿。同名参数、兄弟字段、无 tag 名、定长数组、无 body 时的各种同名变体、表单类型的数组体、multipart 三种形态,另用临时写的对照测试(未提交)比较过改动前后的实际响应。

兼容性

无破坏性变更。in:"body" 是此前不可用的取值:goai 的白名单不认它,配了 OpenAPI 文档的接口在启动生成文档时会报 invalid tag value "body" for In 并退出;没配文档的接口,这个标签被完全忽略。没有任何现存用法依赖它。

唯一新增的语义:声明了该标签的字段,取值只来自请求体——与该字段同名(含大小写、符号变体)的请求参数会被忽略掉。这正是这个特性的契约。

性能与内存影响

一句话:没写 in:"body" 的接口,实测没有变慢、也没有多分配内存;写了的接口,多出来的大头是「解析数组本身」(跟解析对象体是同一套代码),我们自己加的只有几十纳秒;唯一多出来的一次内存分配,出现在「声明了数组字段、但请求没带 body」时把字段置零那一步。

先看普通接口(改动前 d924ae001 → 改动后)

每个样本跑 30000 次,改动前后两份代码在同一个容器里轮流跑、各跑 3 轮,取中位数:

场景 改动前 改动后 内存分配
只建请求(对照组,不做解析) 3292 ns 3137 ns 37 次 / ~6.7KB,两边一模一样
解析普通接口的 JSON 请求体 18538 ns 17574 ns 169 次 / ~17.15KB,一模一样
解析表单请求体 26262 ns 22430 ns 322 次 / ~20.0KB,一模一样
走完整 handler(含反射调用) 20084 ns 18504 ns 174 次 / ~17.3KB,一模一样

时间那列改动后全部略快一点,但这属于测量抖动(两份代码编译出来的程序布局不同、机器也有噪声,±5~15% 很正常),不能当成「变快了」。真正说明问题的是分配次数和内存字节数:一项一项完全相同——普通接口的请求路径上,我们没有多分配任何内存。

再看用上这个特性的接口(改动后)

场景 耗时 内存 分配次数
收 JSON 数组体(3 个元素) ~18.5µs ~18.4KB 176
收表单 content-type 的数组体 ~18.6µs ~18.45KB 176
没带 body(字段置零) ~6.4µs ~9.1KB 67
对照组:普通接口收 JSON 对象体 ~17.6µs ~17.15KB 169
  • 数组体比对象体多花的 +0.9µs / +1.3KB / +7 次分配,花在解析数组本身(把 JSON 变成 []any,3 个元素就要建 3 个 map),不是这次新加的代码。
  • 表单 content-type 收数组体反而更省:parseForm 提前返回,整套表单解码都不做了(原来那套要把 body 切碎、再拼成参数 map)。
  • 「没带 body」这条单独量过(每个样本 50000 次、轮流跑 3 轮;同一个结构体,改动前标签不生效 vs 改动后字段置零):
改动前(普通结构体) 改动后(数组字段、无 body)
耗时 5428 ns 5915(+0.5µs)
内存 ~8813 B ~9108 B(+295B)
分配次数 66 67(多 1 次)

原因很具体:置零要往已经拼好的参数 map 里写一个 nil,map 就得新开一个桶(bucket)。这里有个可以优化、这次没做的办法:注册路由的时候就把「怎么把这个字段置零」算好、存成一个函数(gconv 内部就是这么干的),请求时直接调用、不碰 map,这一次分配就省了。量级是 0.5µs / 1 次分配,真要抠再抠。

这些开销分别落在哪

  • 注册路由时(每条路由一次,不算在请求里):多扫一遍请求结构体找标签;handlerFuncInfo 多两个字符串(每条路由 32 字节);开了 OpenAPI 的进程在生成文档时,每条路由多扫一遍字段(只在启动时)。
  • 请求时、没写标签的接口:三处「字段名是不是空」的判断(parseBody、parseForm、doGetRequestStruct 各一处),只有几纳秒,实测淹没在噪声里。
  • 请求时、写了标签的接口:删掉同名参数、把请求体写进字段、判断请求体形状,合起来几十纳秒,外加参数 map 里多一个条目。
  • 结构体大了多少:Request 每个请求对象多 24 字节(bodyArray 的切片头);因为落在同一个内存档位,实测每请求的内存字节数没变化。

量这些数的方法:Linux 容器(golang:1.25,12 核),改动前后的代码各放一份、轮流跑多轮取中位数,每个样本 30000~50000 次;基准用的是仓库里现成的标准 handler 解析基准(net/ghttp/ghttp_z_bench_parse_test.go,来自 fix/parameter-parsing 分支),另补了「数组体」「没带 body」两个场景,量完就删了、没有提交。

改动文件

文件 改动
net/ghttp/ghttp.go handlerFuncInfo 加 ReqBodyFieldName / ReqBodyFieldTagName
net/ghttp/ghttp_server_service_handler.go checkAndCreateReqBodyField:注册期扫描、校验(按类型给改法提示)、解析两个名字
net/ghttp/ghttp_request.go Request 加 bodyArray
net/ghttp/ghttp_request_param.go parseBody 数组分支;isArrayRequestBodyExpected;parseForm 的表单解码前数组检测
net/ghttp/ghttp_request_param_request.go doGetRequestStruct:删同名参数、绑数组体、拦对象体与 multipart、无 body 时把字段归零
net/goai/goai.go ParameterInBody 常量
net/goai/goai_parameter_ref.go in:"body" 字段不产出 parameter
net/goai/goai_path.go 请求体用字段 schema;findBodyField(body 字段按与 HTTP 注册相同的范围单独扫一份);Properties nil 守卫
net/goai/goai_shema_ref.go 数组 schema 生成前解引用指针,*[]T 不再出数组套数组
net/ghttp/ghttp_z_unit_feature_request_array_body_test.go 新增,端到端用例
net/ghttp/ghttp_z_unit_internal_test.go 新增,注册期校验用例
net/goai/goai_z_unit_test.go 新增 TestOpenApiV3_ArrayRequestBody

…body" field tag

## Problem

A strict route can only receive a JSON object request body. A top-level
JSON array is either rejected with CodeInvalidParameter (under the JSON
content type) or, under the other content types, silently ignored with
all the fields left as zero values and even some bogus parameters parsed
out of the body by the form parameters decoding. There is also no way to
describe such a request body in the generated OpenAPI document.

## Change

The whole request body can now be received by a field tagged with
`in:"body"`, which reuses the existing `in` tag that already declares the
parameter location for path/query/header/cookie:

    type BatchChatReq struct {
        g.Meta   `mime:"application/json" method:"post" path:"/batch/chat"`
        Messages []ChatMessage `json:"messages" in:"body"`
    }

The client then posts `[{"role":"user"}]` as the request body. Only the
slice fields are supported (a pointer to slice included), which is
validated at the route registering time; a fixed-size array can use a
slice with a length validation rule instead. A request parameter named
exactly after the field tag is ignored for that field, so the request
body always takes precedence over the parameters of the same name. The
tag works as the opt-in signal that keeps every other endpoint behaving
exactly as before, and the request body of the generated document is the
schema of that field instead of a wrapping object.
@gqcn

gqcn commented Sep 24, 2026

Copy link
Copy Markdown
Member

数组体绑定这条路径看起来是对的,生成出来的 OpenAPI 和运行时还对不上。

POST/PUT/PATCH 上,没写 in 的字段本来就不会进 parameters(net/goai/goai_parameter_ref.go 里非 GET/DELETE 会直接丢掉),请求体又被换成了 in:"body" 字段的 array schema,这些字段就从文档里消失了。ArrayBodyReq 的 Id 就是这样:?id=123 和 d:"100" 运行时都生效,swagger 里却没有 id。结构体上已经有 in:"body" 时,其余字段按 query 写进 parameters,才和现在的绑定行为一致。

切片字段上的 v:"required" 也没进文档。requestBody.required 现在只看 g.Meta 的 required,字段自己的校验在 newSchemaRefWithGolangType 时传了 nil tag。字段带 required 的话,把 request body 标成必填。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants