feat(cli): propagate display names for x-fern-sdk-group-name hierarchy levels#13300
feat(cli): propagate display names for x-fern-sdk-group-name hierarchy levels#13300
Conversation
Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>
🤖 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:
|
Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>
Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>
| if (method != null) { | ||
| return { group, method }; | ||
| // Use the original group names as display names when they differ from their camelCased versions | ||
| const groupDisplayNames = group.length > 0 ? group : undefined; |
There was a problem hiding this comment.
🟡 The comment on line 264 says "Use the original group names as display names when they differ from their camelCased versions" but line 265 unconditionally sets groupDisplayNames for ALL non-empty groups without comparing against camelCase(group[i]). This causes every subpackage from x-fern-sdk-group-name to get a redundant displayName even when it matches the internal name (e.g., displayName: "chatCompletions", displayName: "user", displayName: "auth"). The fix should filter: only include group[i] in groupDisplayNames when group[i] !== camelCase(group[i]).
Extended reasoning...
What the bug is
In AbstractOperationConverter.computeGroupNameAndLocationFromExtensions() at lines 264-265, there is a mismatch between the comment and the implementation:
// Use the original group names as display names when they differ from their camelCased versions
const groupDisplayNames = group.length > 0 ? group : undefined;The comment explicitly states the intent: display names should only be set when they differ from their camelCased versions. However, the implementation simply checks if the group array is non-empty and passes the entire array as display names unconditionally.
How it manifests
Downstream in AbstractSpecConverter.getOrCreatePackage() (line 593), displayNameParts[i] is directly assigned as the displayName for each subpackage. Since groupDisplayNames is always set for non-empty groups, every subpackage created from an x-fern-sdk-group-name extension now receives a displayName, even when the display name is identical to the internal camelCased name.
This is confirmed by snapshot changes across many test fixtures:
x-fern-streaming-with-reference.json:displayName: "chatCompletions"(same as camelCase)x-fern-global-headers.json:displayName: "user"(same as camelCase)oauth.json:displayName: "auth"(same as camelCase)env-exhaustive-multi-multi.json:displayName: "alpha",displayName: "beta"(same as camelCase)streaming-sse-inferred.json:displayName: "completions"(same as camelCase)
Step-by-step proof
Consider an OpenAPI spec with x-fern-sdk-group-name: ["user"]:
groupNameExtension.convert()returnsgroups: ["user"]group.length > 0istrue, sogroupDisplayNames = ["user"]- This flows through
OperationConverter→OpenAPIConverter.addEndpointToIr()→AbstractSpecConverter.getOrCreatePackage() - In
getOrCreatePackage,camelCase("user")produces"user"as the internal name displayNameParts[i]is"user"(fromgroupDisplayNames), so the subpackage getsdisplayName: "user"- But
"user" === camelCase("user"), so this displayName adds no information — it should have beenundefined
Contrast with the intended case like x-fern-sdk-group-name: ["Tableau APIs"]:
camelCase("Tableau APIs")→"tableauApIs""Tableau APIs" !== "tableauApIs", sodisplayName: "Tableau APIs"is meaningful and correct
Impact
Consumer code (e.g., API reference generators) may interpret the presence of a displayName as indicating a human-readable override is needed. Setting redundant display names on every subpackage could cause unintended rendering changes downstream — for example, displaying "chatCompletions" as a label instead of applying the consumer's own formatting to the internal name.
Fix
The fix should compare each group element against its camelCased form and only include it when they differ:
const groupDisplayNames = group.length > 0
? group.map((g) => g !== camelCase(g) ? g : undefined)
: undefined;Then only set groupDisplayNames to the array if at least one element is non-undefined, or keep it simple and let the downstream code handle undefined entries (which it already does via displayNameParts[i] != null checks in getOrCreatePackage).
Description
Refs: Companion to https://github.com/fern-api/fern-platform/pull/8304
When
x-fern-sdk-group-nameis set with human-readable names (e.g.,["Tableau APIs", "Tableau REST API", "Analytics Extensions Settings Methods"]), the original group names are now preserved asdisplayNameon each subpackage in the IR. Previously, group names were camelCased for internal use but the original human-readable form was lost, resulting in flat/ugly hierarchy names in the generated API reference.Changes Made
groupDisplayNames?: string[]toGroupNameAndLocationandAbstractOperationConverter.OutputAbstractOperationConverter: captures the rawx-fern-sdk-group-namearray values asgroupDisplayNamesOperationConverter/OpenAPIConverter: threadsgroupDisplayNamesthrough toaddEndpointAbstractSpecConverter.getOrCreatePackage: acceptsgroupDisplayNamesand setsdisplayNameon each subpackage in the hierarchyx-fern-sdk-group-namewith space-containing names (Tableau APIs example)Testing
x-fern-sdk-group-namefixture case with 3-level hierarchy)biome checkpassesUpdates Since Last Revision
registerpackage (auth-with-overrides-fdr.snapandauth-with-overrides-ir.snap) where thedisplayNamefor theuserssubpackage changed fromundefinedto"users"due to the new logic.Human Review Checklist
x-fern-sdk-group-namegroups: The current logic setsdisplayNamefor ALL endpoints usingx-fern-sdk-group-name, even when the group name is already camelCase-like (e.g.,"displayName": "chatCompletions"in streaming fixtures). This may cause unintended rendering changes downstream. Consider whether displayName should only be set when the original name contains spaces or differs significantly from its camelCased form.anyof-const-query-parameter,errors-complex,errors-simple,url-reference) have large structural changes (new enum types, modified union representations) that appear unrelated to display names. These look like stale snapshots that got regenerated. Verify these changes are acceptable or revert them.Link to Devin Session
Requested by: @dsinghvi