Skip to content

fix(typescript): reject unresolved query body collisions - #17641

Open
dimitropoulos wants to merge 1 commit into
fern-api:mainfrom
dimitropoulos:fix/typescript-query-body-parameter-collision
Open

fix(typescript): reject unresolved query body collisions#17641
dimitropoulos wants to merge 1 commit into
fern-api:mainfrom
dimitropoulos:fix/typescript-query-body-parameter-collision

Conversation

@dimitropoulos

@dimitropoulos dimitropoulos commented Sep 3, 2026

Copy link
Copy Markdown

Description

Fern generates invalid TypeScript when the request has a body and query parameter that are identical.

Screenshot_20260903_110047

Reject query/body property collisions that would generate invalid TypeScript. Reproduction: https://github.com/dimitropoulos/scratchpad/tree/fern-query-body-parameter-collision (failing CI).

Changes Made

  • Require a distinct x-fern-parameter-name with resolveQueryParameterNameConflicts.
  • Preserve valid overrides and cover inline, referenced, and multipart bodies.
  • Updated README.md generator (not applicable)

Testing

  • Unit tests added/updated
  • pnpm turbo run compile --filter @fern-typescript/request-wrapper-generator
  • pnpm turbo run test --filter @fern-typescript/request-wrapper-generator

Devin Review

Copilot AI lite review requested due to automatic review settings September 3, 2026 15:15

@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.

AI Review Summary

The change turns previously-silent query/body name collisions into hard generation errors and broadens the body-property-name computation. Main concerns: a behavioral regression in the extends handling (nullable type declaration now dereferenced unguarded, and a new throw for non-object extends), redundant recomputation of body property names, and a misleading error message when resolveQueryParameterNameConflicts is already enabled.

  • 🔴 1 critical issue(s)
  • 🟡 3 warning(s)
  • 🔵 2 suggestion(s)

AI Review

🔵 suggestion — generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts (line 1191)

The comment says "Validate that ..." but the variable is still named collidingQueryParamWireValues and the method name (getResolvableQueryParamWireValues) reads as a getter with no hint that it throws. Consider assertQueryParamCollisionsAreResolvable returning the set, or at least a doc comment stating it throws.


To request another review, comment /ai-review on this pull request.

Comment on lines +1112 to +1119
for (let extension of inlinedRequestBody.extends) {
const typeDeclaration = context.type.getTypeDeclaration(extension);
if (typeDeclaration?.shape.type === "object") {
for (const property of typeDeclaration.shape.properties) {
const propName = this.getPropertyNameOfTypeDeclarationProperty(property);
bodyPropertyNames.add(propName.propertyName);
}
if (typeDeclaration.shape.type === "alias" && typeDeclaration.shape.aliasOf.type === "named") {
extension = typeDeclaration.shape.aliasOf;
}
const generatedType = context.type.getGeneratedType(extension);
if (generatedType.type !== "object") {
throw new Error("Inlined request extends a non-object");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 critical

Two regressions here:

  1. context.type.getTypeDeclaration(extension) was previously null-checked (typeDeclaration?.shape.type === "object"); now typeDeclaration.shape is dereferenced unguarded. If the declaration is missing this throws a TypeError instead of skipping.
  2. The old code silently skipped non-object extends; now it throws "Inlined request extends a non-object". This path is reached from collision detection for every endpoint with an inlined body that extends something, so any IR the old code tolerated (e.g. extends an undiscriminated union alias) will now hard-fail generation. If that invariant really holds elsewhere, at least include the extension name in the error for debuggability.

Comment on lines +1070 to +1090
private getResolvableQueryParamWireValues(context: FileContext): Set<string> {
const collidingWireValues = this.getCollidingQueryParamWireValues(context);
const bodyPropertyNames = this.getBodyPropertyNames(context, { includeAllRequestBodyShapes: true });
for (const queryParameter of this.getAllQueryParameters()) {
if (!collidingWireValues.has(getWireValue(queryParameter.name))) {
continue;
}
const propertyName = this.getPropertyNameOfQueryParameter(queryParameter).propertyName;
const overriddenPropertyName = this.getOverriddenPropertyNameOfQueryParameter(queryParameter).propertyName;
if (
!this.resolveQueryParameterNameConflicts ||
overriddenPropertyName === propertyName ||
bodyPropertyNames.has(overriddenPropertyName)
) {
throw new Error(
`Cannot generate ${this.wrapperName}: query parameter "${propertyName}" conflicts with a request body property. Set a distinct x-fern-parameter-name and enable resolveQueryParameterNameConflicts.`
);
}
}
return collidingWireValues;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

getResolvableQueryParamWireValues recomputes getBodyPropertyNames(..., includeAllRequestBodyShapes: true) even though getCollidingQueryParamWireValues just built the same set internally — and this whole function is called three times per wrapper (getRequestProperties, getNonBodyKeys, getNonBodyKeysWithData). That's 6 full traversals of the body (each doing getGeneratedType / getAllPropertiesIncludingExtensions) per wrapper. Consider having getCollidingQueryParamWireValues return both sets, and memoizing the result per context.

bodyPropertyNames.has(overriddenPropertyName)
) {
throw new Error(
`Cannot generate ${this.wrapperName}: query parameter "${propertyName}" conflicts with a request body property. Set a distinct x-fern-parameter-name and enable resolveQueryParameterNameConflicts.`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

The error message tells the user to "Set a distinct x-fern-parameter-name and enable resolveQueryParameterNameConflicts" even when resolveQueryParameterNameConflicts is already true and the override itself collides (see the test at line 374). Branch the message on which condition failed so users aren't told to enable a flag they already enabled, and mention the colliding override name in that case.

return;
}
}
bodyPropertyNames.add(this.getReferencedBodyPropertyName());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

bodyPropertyNames.add(this.getReferencedBodyPropertyName()) now runs for every non-flattened reference body, meaning any query param named body (or whatever the referenced-body key is) now hard-fails generation where it previously generated fine. That's a real breaking change for existing users, not just invalid-TS rejection. Worth confirming this is intended and calling it out in the changelog entry.

bodyPropertyNames.add(propKey.propertyName);
}
for (const extension of inlinedRequestBody.extends) {
for (let extension of inlinedRequestBody.extends) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

let extension reassigned inside the loop is a bit sneaky. A local (const resolved = ...) reads better and avoids accidental reuse of the mutated loop variable later.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

Copilot AI 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.

🟡 Changes recommended

The new thrown error message gives misleading guidance when conflict resolution is already enabled, which can confuse users and hinder debugging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR hardens the TypeScript request-wrapper generator by detecting query-parameter vs request-body property name collisions early and failing fast instead of emitting invalid TypeScript, with additional coverage for referenced and multipart/file-upload bodies.

Changes:

  • Introduces unconditional collision validation via getResolvableQueryParamWireValues(...), requiring a distinct x-fern-parameter-name when conflict resolution is enabled.
  • Expands body-property name collection to cover additional request body shapes (referenced bodies and file upload properties) to properly detect collisions.
  • Adds unit tests covering unresolved collisions, valid resolutions, and edge cases (literal body properties, referenced bodies, inline file properties), plus an unreleased changelog entry.
File summaries
File Description
generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts Adds collision validation and broadens body property name detection across request body shapes.
generators/typescript/sdk/request-wrapper-generator/src/test/GeneratedRequestWrapperImpl.test.ts Adds tests for collision rejection/resolution scenarios (inline, referenced, multipart/file upload).
generators/typescript/sdk/changes/unreleased/reject-unresolved-query-body-collisions.yml Records the generator fix in the changelog.
Review details

Suppressed comments (2)

generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts:1086

  • The thrown error message always says to "enable resolveQueryParameterNameConflicts", even in cases where it is already enabled and the real problem is that the x-fern-parameter-name override is not distinct (or it collides with another body property). This can mislead users debugging generation failures.
                throw new Error(
                    `Cannot generate ${this.wrapperName}: query parameter "${propertyName}" conflicts with a request body property. Set a distinct x-fern-parameter-name and enable resolveQueryParameterNameConflicts.`
                );

generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts:1095

  • This comment still says "(inlined) request body", but getBodyPropertyNames can include referenced and file-upload request bodies when includeAllRequestBodyShapes is true. Updating the comment will make the collision logic easier to reason about.
    /**
     * Computes the set of property names produced by the (inlined) request body. Used to detect
     * collisions between non-body parameters (path/query) and body property names.
     */
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 1052 to 1056
/**
* When resolveQueryParameterNameConflicts is enabled, computes the set of query parameter
* wire values that collide with body property names. Only these colliding query params
* will use their SDK override names instead of wire values.
*/
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