Skip to content

fix(openapi): collapse dotted operationIds into valid endpoint names - #17310

Merged
dvdaruri-art merged 4 commits into
mainfrom
devin/1785509122-fix-dotted-operation-id
Jul 31, 2026
Merged

fix(openapi): collapse dotted operationIds into valid endpoint names#17310
dvdaruri-art merged 4 commits into
mainfrom
devin/1785509122-fix-dotted-operation-id

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

An OpenAPI operation whose operationId contains a dot and whose leading token does not match its tag (e.g. actions.index under tag Custom Actions) is imported as an endpoint literally named actions.index in customActions.yml. A dot in a Fern endpoint name is parsed as a cross-file reference (import.endpoint), so x-fern-pagination — the one path that resolves an endpoint by its own key (convertPaginationPropertyResolverEndpointResolver) — fails with:

Cannot resolve endpoint: actions.index in file customActions.yml

comments.index under tag Comments works today only because the tag/operationId prefixes overlap, which makes the importer camelCase the remaining tokens.

Changes Made

  • getEndpointLocation.ts (v2 importer): the two branches that pass operationId straight through as the endpoint id (no tag, and tag/operationId mismatch) now run it through
    operationId.includes(".") ? camelCase(operationId) : operationId
    so actions.indexactionsIndex. Non-dotted ids are untouched, so no existing snapshot changed.
  • AbstractOperationConverter.evaluateMethodNameFromOperation (v3 importer): same collapse, so the v3 path no longer emits a dotted method name into the IR. Tokenization is unaffected (tokenizeString("actionsIndex") === tokenizeString("actions.index")), so grouping is unchanged.
  • New x-fern-pagination-tag-mismatch fixture (Comments/comments.index vs Custom Actions/actions.index, both paginated) in both importer test suites, plus a CLI changelog entry.

Testing

  • Unit tests added/updated — new fixture + snapshots in openapi-ir-to-fern-tests and v3-importer-tests; pnpm test:update produced no diffs on existing snapshots.

  • Manual testing completed — built the CLI and ran fern ir against the fixture project:

    before: [api]: Cannot resolve endpoint: actions.index in file customActions.yml (exit 1)
    after: [api]: ✓ All checks passed / Wrote IR to /tmp/ir.json, with the endpoint named actionsIndex under service customActions.

Link to Devin session: https://app.devin.ai/sessions/8fd92fc8964840f4a6b9e314385d65c9
Requested by: @fern-support


Open in Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@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.

AI Review Summary

Small, targeted fix: dotted operationIds are collapsed via camelCase so they don't get parsed as cross-file references (import.endpoint). Logic is duplicated verbatim in the v2 and v3 importers, and the collapse can silently collide with sibling endpoint names. Fixtures/snapshots look consistent with the described behavior.

  • 🟡 1 warning(s)
  • 🔵 2 suggestion(s)

Comment on lines +30 to +34
// A dot in an endpoint id is parsed as a reference to another file (`import.endpoint`),
// so dotted operation ids must be collapsed into a single name.
function sanitizeEndpointId(operationId: string): string {
return operationId.includes(".") ? camelCase(operationId) : operationId;
}

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

Collapsing foo.barfooBar can silently collide with an existing fooBar operationId under the same tag, which would overwrite one endpoint in the generated file rather than erroring. Worth at least documenting, and ideally detecting the duplicate at build time (the endpoint map presumably just gets clobbered).

Also note this is a user-visible rename for anyone who already has dotted operationIds and isn't using pagination — their SDK method names change. Make sure the changelog entry calls that out.

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.

Changelog updated in 07d3cca to call out the rename explicitly (dotted ids are now imported as e.g. actionsIndex; x-fern-sdk-method-name pins a specific name).

On the collision: agreed it's theoretically possible (foo.bar + fooBar under the same tag), but note the collapse only runs on ids that contain a dot, and an id like foo.bar is currently unusable for anything that resolves endpoints by key — so the realistic blast radius is small. Proper duplicate detection would need to live in the endpoint-map construction (both importers), which is beyond this fix; flagging it to the requester as a possible follow-up rather than expanding scope here.

Comment on lines +332 to +334
// A dot in a method name is parsed as a reference to another file (`import.endpoint`),
// so dotted operation ids must be collapsed into a single name.
return operationId.includes(".") ? camelCase(operationId) : operationId;

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

This is the exact same predicate + comment as sanitizeEndpointId in openapi-ir-to-fern/src/utils/getEndpointLocation.ts. Since the two importers must stay in agreement (v2/v3 snapshot parity), consider exporting a single shared helper (e.g. from a commons package) so a future tweak doesn't drift between them.

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.

Fair, though after e3f6dc2 the two are no longer identical in placement: v2 sanitizes the endpoint id in getEndpointLocation, v3 sanitizes the method name after tokenization in computeGroupNameFromTagAndOperationId. The shared part is a one-line predicate, and the v2 package (openapi-ir-to-fern) doesn't currently depend on importer-commons, so extracting it would mean a new cross-package dependency. Leaving as-is for this bugfix; the new x-fern-pagination-tag-mismatch fixture exists in both suites specifically to catch drift between them.

// A dot in an endpoint id is parsed as a reference to another file (`import.endpoint`),
// so dotted operation ids must be collapsed into a single name.
function sanitizeEndpointId(operationId: string): string {
return operationId.includes(".") ? camelCase(operationId) : operationId;

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

Only . is handled here, but other characters are equally hostile to Fern names (spaces, /, :, leading digits). Not required for this fix, but if the intent is "produce a valid endpoint name", a general validity check (/^[A-Za-z_][A-Za-z0-9_]*$/) would be more robust than a dot sniff — otherwise the next report is actions index.

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.

Agreed that . isn't the only hostile character, but it is the only one that changes parsing semantics (import.endpoint), which is what causes the reported failure — spaces/:// produce ugly names, not resolution errors. Broadening to a full validity check would rename endpoints for specs that work today, so it's a behavior change I don't want to slip into a bugfix. Raising it with the requester as a separate item.

@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 +332 to +334
// A dot in a method name is parsed as a reference to another file (`import.endpoint`),
// so dotted operation ids must be collapsed into a single name.
return operationId.includes(".") ? camelCase(operationId) : operationId;

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.

🟡 Some imported endpoints get a redundant group prefix in their generated name

The operation name is rewritten before it is split into words for grouping (camelCase(operationId) at packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/AbstractOperationConverter.ts:334), so for names that mix dots with digits the shared prefix with the tag is no longer detected and stays in the final name.
Impact: Affected endpoints are generated as e.g. users.usersListV2 instead of users.listV2, and differ from what the older importer produces for the same spec.

Tokenization changes when camelCase merges digit boundaries

computeGroupNameFromTagAndOperationId (packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/AbstractOperationConverter.ts:337-357) tokenizes the value returned by evaluateMethodNameFromOperation. tokenizeString only splits on capital letters when the input matches /^[a-z]+(?:[A-Z][a-z]+)*$/ (...:406), which fails as soon as a digit is present.

Example: tag Users, operationId users.list_v2.

  • Before: tokenizeString("users.list_v2")["users","list","v2"]; tag is a prefix, so method = camelCase("list_v2") = listV2, group [Users].
  • After: camelCase("users.list_v2") = usersListV2; tokenizeString("usersListV2")["userslistv2"] (single token, because the digit disables the camel-case split), so the tag is no longer a prefix and computeGroupAndMethodFromTokens (...:359-384) returns method usersListV2 under group [Users].

The v2 importer avoids this because it collapses the dots after tokenizing (packages/cli/api-importers/openapi/openapi-ir-to-fern/src/utils/getEndpointLocation.ts:78-110), so the two importers now disagree for these operation ids.

Prompt for agents
In packages/cli/api-importers/openapi-to-ir/src/3.1/paths/operations/AbstractOperationConverter.ts, evaluateMethodNameFromOperation now returns camelCase(operationId) when the operationId contains a dot. That value is consumed by computeGroupNameFromTagAndOperationId, which tokenizes it via tokenizeString before comparing against the tag tokens. tokenizeString only splits on capital letters when the string matches /^[a-z]+(?:[A-Z][a-z]+)*$/, so any camelCased name containing a digit (e.g. camelCase('users.list_v2') === 'usersListV2') collapses to a single token, and the tag prefix is no longer stripped — producing method names like usersListV2 under group Users instead of listV2. The v2 importer (openapi-ir-to-fern/src/utils/getEndpointLocation.ts) avoids this by tokenizing the raw operationId and only collapsing dots on the branches that emit the operationId verbatim. Consider mirroring that: keep tokenizing the raw operationId for grouping and apply the dot collapse only where the final method name is produced (the tag == null branch and the non-prefix branch), or make tokenizeString digit-aware.
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.

Good catch — confirmed: tokenizeString("users.list_v2")["users","list","v2"] but tokenizeString(camelCase("users.list_v2")) = tokenizeString("usersListV2")["userslistv2"], so the tag prefix stopped being stripped for ids with digits.

Fixed in e3f6dc2 by mirroring the v2 importer: evaluateMethodNameFromOperation returns the raw operationId again, and the dot collapse moved into a sanitizeMethodName helper applied only where the final method name is emitted (the tag == null branch and the non-prefix branch), i.e. after tokenization.

Added Users + users.list_v2 to the x-fern-pagination-tag-mismatch fixture; both importers now produce users.listV2.

…mporter

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

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-31T05:08:23Z).

Fixture main PR Delta
docs 256.7s (n=5) 237.1s (35 versions) -19.6s (-7.6%)

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-07-31T05:08:23Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-31 22:20 UTC

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-31T05:08:23Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 80s (n=5) N/A 50s -30s (-37.5%)
go-sdk square 143s (n=5) 308s (n=5) 112s -31s (-21.7%)
java-sdk square 225s (n=5) 287s (n=5) 164s -61s (-27.1%)
php-sdk square 78s (n=5) N/A 44s -34s (-43.6%)
python-sdk square 143s (n=5) 259s (n=5) 149s +6s (+4.2%)
ruby-sdk-v2 square 109s (n=5) 151s (n=5) 85s -24s (-22.0%)
rust-sdk square 213s (n=5) 215s (n=5) 240s +27s (+12.7%)
swift-sdk square 78s (n=5) 457s (n=5) 57s -21s (-26.9%)
ts-sdk square 178s (n=5) 188s (n=5) 94s -84s (-47.2%)

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-07-31T05:08:23Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-31 22:23 UTC

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@dvdaruri-art
dvdaruri-art enabled auto-merge (squash) July 31, 2026 22:12
@dvdaruri-art
dvdaruri-art merged commit 1624894 into main Jul 31, 2026
219 checks passed
@dvdaruri-art
dvdaruri-art deleted the devin/1785509122-fix-dotted-operation-id branch July 31, 2026 22:20
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.

3 participants