feat(go): respect an omittable request body behind respectOptionalRequestBody - #17378
Conversation
…uestBody Co-Authored-By: bot_apk <apk@cognition.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| private callOmitsRequestBody({ | ||
| request, | ||
| snippet | ||
| }: { | ||
| request: FernIr.dynamic.BodyRequest; | ||
| snippet: FernIr.dynamic.EndpointSnippetRequest; | ||
| }): boolean { | ||
| if (this.context.customConfig?.respectOptionalRequestBody !== true) { | ||
| return false; | ||
| } | ||
| if (request.bodyRequired !== false) { | ||
| return false; | ||
| } | ||
| const value = snippet.requestBody; | ||
| return value == null || (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0); | ||
| } |
There was a problem hiding this comment.
🟡 Generated code examples can pass "nothing" where the client requires a real value, so the example does not compile
The snippet generator decides to render an omitted body (callOmitsRequestBody at generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts:943-958) without checking that the body's Go type can actually hold "nothing", unlike the client generator which does check, so for such bodies the example passes a value the client method cannot accept.
Impact: Users copying the generated example for such an endpoint get code that fails to build.
Divergence between the snippet predicate and the SDK predicate on nilability
generators/go-v2/sdk/src/utils/mayOmitRequestBody.ts:26 gates the SDK-side behaviour on goTypeMapper.convert(...).isNilable(), so a referenced body whose Go type is a value type (e.g. a string/float64 primitive body, or a named alias, which GoTypeMapper.convertNamed at generators/go-v2/base/src/context/GoTypeMapper.ts:159-171 maps to a non-pointer Type.reference) keeps being sent and the generated parameter stays a value type (ReferencedEndpointRequest.getRequestParameterType).
callOmitsRequestBody only checks respectOptionalRequestBody plus request.bodyRequired === false, so for the same endpoint the snippet takes the nop branch at generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts:242-254 and emits nil as the positional argument — cannot use nil as string value in Go.
The fix is to mirror the nilability check on the snippet side (the dynamic snippet context has its own type mapper), or to restrict omission to body shapes whose Go type is a pointer/slice/map/any.
Prompt for agents
In generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts, callOmitsRequestBody decides to render `nil` for an omitted request body based only on the respectOptionalRequestBody flag and request.bodyRequired === false. The SDK generator's equivalent predicate (generators/go-v2/sdk/src/utils/mayOmitRequestBody.ts) additionally requires the body's Go type to be nilable (pointer/slice/map/any), because a value-typed body parameter (e.g. a primitive string/number body, or a named alias which GoTypeMapper.convertNamed maps to a bare reference rather than a pointer) cannot accept nil. When the two predicates disagree, the generated snippet passes nil to a value-typed parameter and does not compile. Align the snippet-side predicate with the SDK-side one by determining nilability of the body type reference (via the dynamic snippets type mapper / context) before treating the body as omittable.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — fixed in 3522697. callOmitsRequestBody now mirrors the SDK-side nilability requirement via bodyParameterIsNilable: a typeReference body goes through context.dynamicTypeMapper.convert(...).isNilable() (bytes is always nilable, since it is either []byte or an io.Reader), so a value-typed body such as a bare string keeps rendering a value instead of nil.
Added a test that retypes the fixture's optional bulkRefund body as a STRING and asserts the snippet still emits request := "re_1234".
Co-Authored-By: bot_apk <apk@cognition.ai>
Description
Refs #17360
Brings the Go SDK generator to parity with Java (#17372), Python (#17370), TypeScript (#17368), and C# (#17373) on IR request-body omittability, stacked on
feat/ir-request-body-required.New Go config flag
respectOptionalRequestBody(camelCase, matching the rest ofbaseGoCustomConfigSchema). Off by default. When it is on and the IR saysrequired: falseon a referenced request body, a call that supplies no body sends neither a body nor aContent-Typeheader — including the wrapper-present/Body-nil case, which is exactly the shape that sent the JSON literalnullto Payabli's refund endpoint.Two halves, because the header half is what Python initially missed:
Bodystraight to the caller marshals asnull.raw_client.gonow unpacks it first:Content-Type— the caller sets the header unconditionally while building the request, so it is dropped again once the built request turns out to have no body:Both caller pieces (the
CallParams.BodyIsOptionalfield and the check) come from placeholders ininternal/caller.go_that collapse to nothing when the flag is off, so an SDK that has not opted in gets byte-identical output — regeneratingexhaustive,optional,file-upload, andpaginationproduced no diff.Only a referenced body whose Go type can be compared to nil is affected (
mayOmitRequestBody): an absent IRrequiredstill means required, and a body that cannot express its own absence keeps being sent.Changes Made
respectOptionalRequestBodyadded tobaseGoCustomConfigSchemamayOmitRequestBody— the single predicate for "the caller may leave this body out" (opt-in +requestBody.type === "reference"+required === false+ nilable Go type), consumed by both the wrapper and the callerWrappedEndpointRequestunpacks the wrapper's body into a local so a nilBodybecomes no body at all;CallerpassesBodyIsOptional: trueinternal/caller.go_+GoProject— flag-gatedCallParams.BodyIsOptionaland theContent-TyperemovalbodyRequiredand passnilfor an omitted body (go.TypeInstantiation.nop(), so no dangling argument delimiter) — same shape as the TS/C# implementations@fern-fern/ir-sdk→67.21.0(go-v2 base/model/sdk) and@fern-api/dynamic-ir-sdk→67.21.0(go-v2 dynamic-snippets);required/bodyRequiredonly exist there. Both are unpublished until feat(ir): model request-body omittability separately from its type #17360 merges, so install/compile is red until then andpnpm-lock.yamlcannot be updated yet.go-optional-request-bodyfixture: a$ref'dWateringRequestshared by four endpoints (optional with a path param, optional bare body, required baseline, optional alongside a header). Shared so the importer keeps it a reference — a single-use$refgets inlined and dropsrequired, which would make the flag silently inert.fern iron the fixture shows"type": "reference","required": falsefor the three optional endpoints and norequiredforprunePlantgenerators/go/sdk/changes/unreleased/Testing
generators/go-v2/dynamic-snippets/src/__test__/OptionalRequestBody.test.ts(106 dynamic-snippet tests pass, snapshots unchanged)httptestserver echoing backContent-Type/body, both fixture variants:A required body is untouched in both columns, and every explicit body is identical across them.
Also run:
pnpm seed test --generator go-sdk --fixture go-optional-request-body(2/2), the four regression fixtures above (7/7, no diff),pnpm turbo run compile --filter @fern-api/go-sdk --filter @fern-api/go-dynamic-snippets,pnpm format.Link to Devin session: https://app.devin.ai/sessions/e049388014364ff2b5f435342e1d5f25