Skip to content

feat(typescript): let the caller omit an optional request body - #17368

Open
devin-ai-integration[bot] wants to merge 7 commits into
feat/ir-request-body-requiredfrom
devin/1786391952-ts-optional-request-body
Open

feat(typescript): let the caller omit an optional request body#17368
devin-ai-integration[bot] wants to merge 7 commits into
feat/ir-request-body-requiredfrom
devin/1786391952-ts-optional-request-body

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

First generator adoption of HttpRequestBodyReference.required from #17360, which this PR is based on.

required: false means the call may omit the body; the body type itself is untouched. So the parameter's question mark now comes from required rather than from the type being optional<Body>:

- public refund(id: string, request: RefundRequest, ...)
+ public refund(id: string, request?: RefundRequest, ...)

Three call shapes, all driven by the same field:

endpoint shape generated
path param + omittable body refund(id, request?: RefundRequest)
omittable body only bulkRefund(request?: RefundRequest)
omittable body + header (wrapper) body?: RefundRequest in the wrapper, so request: RefundWithHeaderRequest = {}

Absent required still means required, so every endpoint predating the field generates byte-identical output — git diff feat/ir-request-body-required -- seed/ touches only the new fixture.

Serialization needed one change. An omittable body is typed as Body, not optional<Body>, so its schema rejects undefined; the body is now serialized only once the caller supplies one:

body: mergeAdditionalBodyParameters(request == null ? undefined : request, requestOptions?.additionalBodyParameters)

Blocked on the IR release. @fern-fern/ir-sdk is pinned at 67.15.0 here and the latest published is 67.20.0; required arrives in 67.21.0, which only exists once #17360 merges and releases. Compile will be red until then, at which point the pin moves to 67.21.0 in a follow-up commit. IR parsing passes unrecognized keys through, so the seed evidence below is real: the local CLI produced IR carrying required: false and the generator read it.

Changes Made

  • RequestBodyParameter — parameter is optional when required === false, keeping requestBodyType as the parameter type
  • GeneratedRequestWrapperImpl — same for the wrapper's body property, both flattened and non-flattened
  • GeneratedDefaultEndpointRequest — skip serializing an omitted body
  • New fixture ts-optional-request-body exercising the four shapes above (including a required body as the control), language-prefixed so it runs for ts-sdk only
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated — client-class-generator 607 passed, request-wrapper-generator 166 passed
  • Manual testing completed — seed test --generator ts-sdk --fixture ts-optional-request-body and a regression pass over respect-optional-request-body and exhaustive (no output change)

Link to Devin session: https://app.devin.ai/sessions/1795b2f90c804736b12138567df6e981


Open in Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the changes — everything looks good. No issues found.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +292 to +314
private mayOmitRequestBody(): boolean {
return this.requestBody?.type === "reference" && this.requestBody.required === false;
}

/**
* An omittable body is typed as the body itself rather than `optional<Body>`, so its schema
* rejects `undefined`. Serialize only once the caller has supplied a body.
*/
private skipSerializationWhenBodyIsOmitted(
referenceToRequestBody: ts.Expression,
serializedRequestBody: ts.Expression
): ts.Expression {
return ts.factory.createConditionalExpression(
ts.factory.createBinaryExpression(
referenceToRequestBody,
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken),
ts.factory.createNull()
),
undefined,
ts.factory.createIdentifier("undefined"),
undefined,
serializedRequestBody
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Omittable request body is still sent when request-parameter flattening is turned on

The body is only skipped when the value the caller passed is empty (request == null ? undefined : ... at generators/typescript/sdk/client-class-generator/src/endpoint-request/GeneratedDefaultEndpointRequest.ts:300-314), but with request-parameter flattening the value being checked is always a freshly built object, so an empty body is still sent with a JSON content type instead of no body at all.
Impact: For SDKs generated with flattening enabled, endpoints whose body may be omitted always send an empty JSON body, which servers may reject or treat differently from a bodyless request.

Why the emitted null check can never fire under flattenRequestParameters

With flattenRequestParameters: true and a named object body, hasBodyProperty returns false (generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts:871-899) and areBodyPropertiesInlined() returns true (generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts:457-462). RequestWrapperParameter.getReferenceToRequestBody therefore returns either the rest-spread variable _body (generators/typescript/sdk/client-class-generator/src/request-parameter/RequestWrapperParameter.ts:90-98,122-134) or the wrapper parameter itself, which defaults to {} when all properties are optional. Both are always non-null objects, so mayOmitRequestBody() emits a check that never evaluates true and serializers...jsonOrThrow({}) runs, producing an empty JSON body plus a Content-Type header.

Relatedly, the flattened branch of getFlattenedReferencedRequestBodyProperties (the named-object path, generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts:961-990) does not consult mayOmitReferencedBody, so required properties of a body that may be omitted entirely remain required on the wrapper interface.

Prompt for agents
When a referenced request body carries `required: false` and the generator runs with `flattenRequestParameters` enabled, the new omit handling in GeneratedDefaultEndpointRequest (mayOmitRequestBody / skipSerializationWhenBodyIsOmitted) is ineffective: for a named object body the wrapper flattens the body into individual properties, so `getReferenceToRequestBody` yields the rest-spread `_body` variable (or the wrapper parameter, which defaults to `{}`), neither of which can ever be null. The generated `x == null ? undefined : serialize(x)` therefore always serializes, and the SDK sends `{}` with a JSON Content-Type instead of omitting the body. Additionally, getFlattenedReferencedRequestBodyProperties only applies mayOmitReferencedBody on the non-flattened fallback path, so required properties of an omittable body stay required in the wrapper interface. Consider either disabling flattening for omittable referenced bodies, or emitting an emptiness check the flattened case can actually satisfy (e.g. checking that none of the flattened body keys were supplied) and propagating omittability onto the flattened properties.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the mechanism, and it is a real gap — but only under flattenRequestParameters, which defaults to false (sdk/cli/src/SdkGeneratorCli.ts:97). With it on, a named object body flattens into individual wrapper properties, so getReferenceToRequestBody yields _body/the wrapper (never null) and the emitted x == null check can't fire; getFlattenedReferencedRequestBodyProperties likewise doesn't consult omittability.

It isn't a regression: today those endpoints get a required body under that config, so the flattened output is unchanged by this PR — omission simply isn't expressed there yet.

Closing it needs a product call rather than a mechanical fix, because "the body was omitted" has no representation once the body's properties are the parameters: either omittable bodies opt out of flattening (changes signatures for flattening users), or the generator emits "none of the body keys were supplied" and makes the flattened properties optional (widens required properties to optional, and makes an all-defaults call ambiguous with an explicitly empty body). Leaving as-is for this PR and raising it with @will.kendall.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-10T04:30:32Z).

Fixture main PR Delta
docs 260.2s (n=5) 249.9s (35 versions) -10.3s (-4.0%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-08-10T04:30:32Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-11 02:49 UTC

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-10T04:30:32Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 90s (n=5) N/A 95s +5s (+5.6%)
go-sdk square 145s (n=5) 303s (n=5) 129s -16s (-11.0%)
java-sdk square 233s (n=5) 281s (n=5) 202s -31s (-13.3%)
php-sdk square 80s (n=5) N/A 54s -26s (-32.5%)
python-sdk square 151s (n=5) 256s (n=5) 135s -16s (-10.6%)
ruby-sdk-v2 square 109s (n=5) 140s (n=5) 106s -3s (-2.8%)
rust-sdk square 228s (n=5) 236s (n=5) 196s -32s (-14.0%)
swift-sdk square 78s (n=5) 453s (n=5) 53s -25s (-32.1%)
ts-sdk square 176s (n=5) 189s (n=5) 43s -133s (-75.6%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-08-10T04:30:32Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-11 02:50 UTC

willkendall01 and others added 3 commits August 10, 2026 21:26
Reads the IR's new request-body `required` field: `false` makes the body parameter optional
while keeping its own type, so `refund(id, request?: RefundRequest)` rather than a parameter
typed `optional<RefundRequest>`. An absent body is not serialized.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ture

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
With the flag on, an endpoint whose IR request body carries `required: false` takes an
optional body parameter and sends no body when the caller omits it; the parameter keeps the
body's own type instead of widening to `Body | undefined`. Defaults to off, so existing
signatures are untouched until a user opts in.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1786391952-ts-optional-request-body branch from 99b003c to 19a6ba4 Compare August 10, 2026 21:36
…ature

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration Bot and others added 3 commits August 10, 2026 23:59
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

1 participant