Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,10 @@ export function generateIr({
tags: {
tagsById: Object.fromEntries(
(openApi.tags ?? []).map((tag) => {
return [tag.name, { id: tag.name, description: tag.description }];
const xDisplayName = (tag as unknown as Record<string, unknown>)["x-displayName"];
const displayName =
typeof xDisplayName === "string" && xDisplayName.trim().length > 0 ? xDisplayName : undefined;
return [tag.name, { id: tag.name, description: tag.description, displayName }];
})
),
orderedTagIds: openApi.tags?.map((tag) => tag.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function buildServices(context: OpenApiIrConverterContext): ConvertedServ
});
if (irTag?.id != null || irTag?.description != null) {
context.builder.setServiceInfo(file, {
"display-name": group?.summary ?? irTag?.id,
"display-name": group?.summary ?? irTag?.displayName ?? irTag?.id,
docs: group?.description ?? irTag?.description ?? undefined
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ types:
extends: WithDescription
properties:
id: TagId
displayName:
type: optional<string>
docs: Read from `x-displayName` extension on the tag object.

SecuritySchemeId:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ import type * as FernOpenapiIr from "../../../index.js";

export interface Tag extends FernOpenapiIr.WithDescription {
id: FernOpenapiIr.TagId;
/** Read from `x-displayName` extension on the tag object. */
displayName: string | undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { WithDescription } from "./WithDescription.js";
export const Tag: core.serialization.ObjectSchema<serializers.Tag.Raw, FernOpenapiIr.Tag> = core.serialization
.objectWithoutOptionalProperties({
id: TagId,
displayName: core.serialization.string().optional(),
})
.extend(WithDescription);

export declare namespace Tag {
export interface Raw extends WithDescription.Raw {
id: TagId.Raw;
displayName?: string | null;
}
}
12 changes: 12 additions & 0 deletions packages/cli/cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 4.46.3
changelogEntry:
- summary: |
Support `x-displayName` extension on OpenAPI tags. When present, the
display name is used as the service display name in the Fern definition
and as the sidebar title in generated documentation, instead of applying
`titleCase` to the raw tag name. This prevents tag names like "WhatsApp"
from being mangled to "Whats App".
type: fix
createdAt: "2026-03-26"
irVersion: 65

- version: 4.46.2
changelogEntry:
- summary: |
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ export class ApiReferenceNodeConverter {
private workspace?: FernWorkspace,
private hideChildren?: boolean,
private parentAvailability?: docsYml.RawSchemas.Availability,
private openApiTags?: Record<string, { id: string; description: string | undefined }>,
private openApiTags?: Record<
string,
{ id: string; description: string | undefined; displayName: string | undefined }
>,
graphqlNamespacesByOperationId?: Map<FdrAPI.GraphQlOperationId, string>
) {
this.#tagDescriptionContent = new Map();
Expand Down Expand Up @@ -208,7 +211,8 @@ export class ApiReferenceNodeConverter {

// Store the tag description content (no manual escaping needed;
// the downstream MDX pipeline handles sanitization)
const markdownContent = `# ${titleCase(tagInfo.id.replace(/[_-]/g, " "))}\n\n${tagInfo.description}`;
const heading = tagInfo.displayName ?? titleCase(tagInfo.id.replace(/[_-]/g, " "));
const markdownContent = `# ${heading}\n\n${tagInfo.description}`;
this.#tagDescriptionContent.set(virtualAbsolutePath, markdownContent);

// Add to markdown files collections for processing
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/docs-resolver/src/DocsDefinitionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,7 +1352,9 @@ export class DocsDefinitionResolver {
}

// Extract OpenAPI IR tags when tag description pages are enabled
let openApiTags: Record<string, { id: string; description: string | undefined }> | undefined;
let openApiTags:
| Record<string, { id: string; description: string | undefined; displayName: string | undefined }>
| undefined;
if (item.tagDescriptionPages && useV3Parser) {
try {
const workspaceForTags = openapiWorkspace ?? this.getOpenApiWorkspaceForApiSection(item);
Expand All @@ -1371,7 +1373,11 @@ export class DocsDefinitionResolver {
.filter(([_, tag]) => tag.description && tag.description.trim().length > 0)
.map(([tagId, tag]) => [
camelCase(tagId),
{ id: String(tag.id), description: tag.description }
{
id: String(tag.id),
description: tag.description,
displayName: tag.displayName
}
])
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,13 @@ describe("tag-description-pages", () => {
});

// Provide openApiTags with descriptions containing special characters
const openApiTags: Record<string, { id: string; description: string | undefined }> = {
const openApiTags: Record<
string,
{ id: string; description: string | undefined; displayName: string | undefined }
> = {
pet: {
id: "pet",
displayName: undefined,
description: [
"Everything about your Pets.",
"",
Expand All @@ -184,13 +188,18 @@ describe("tag-description-pages", () => {
"Filter pets with query params like `status=available` or `tags=<indoor>`."
].join("\n")
},
store: { id: "store", description: "Access to Petstore orders" },
user: { id: "user", description: "Operations about user" },
store: { id: "store", description: "Access to Petstore orders", displayName: undefined },
user: { id: "user", description: "Operations about user", displayName: undefined },
studyCollections: {
id: "Study Collections",
description: "Manage study collections and their contents"
description: "Manage study collections and their contents",
displayName: undefined
},
userManagement: { id: "user-management", description: "User management operations" }
userManagement: {
id: "user-management",
description: "User management operations",
displayName: undefined
}
};

const converter = new ApiReferenceNodeConverter(
Expand Down Expand Up @@ -299,15 +308,23 @@ describe("tag-description-pages", () => {
context
});

const openApiTags: Record<string, { id: string; description: string | undefined }> = {
pet: { id: "pet", description: "Everything about your Pets" },
store: { id: "store", description: "Access to Petstore orders" },
user: { id: "user", description: "Operations about user" },
const openApiTags: Record<
string,
{ id: string; description: string | undefined; displayName: string | undefined }
> = {
pet: { id: "pet", description: "Everything about your Pets", displayName: undefined },
store: { id: "store", description: "Access to Petstore orders", displayName: undefined },
user: { id: "user", description: "Operations about user", displayName: undefined },
studyCollections: {
id: "Study Collections",
description: "Manage study collections and their contents"
description: "Manage study collections and their contents",
displayName: undefined
},
userManagement: { id: "user-management", description: "User management operations" }
userManagement: {
id: "user-management",
description: "User management operations",
displayName: undefined
}
};

const converter = new ApiReferenceNodeConverter(
Expand Down
4 changes: 4 additions & 0 deletions test-definitions/fern/apis/tag-display-names/generators.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json
api:
specs:
- openapi: ./openapi.yml
116 changes: 116 additions & 0 deletions test-definitions/fern/apis/tag-display-names/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
openapi: 3.1.0
info:
title: tag-display-names
description: Tests that x-displayName on tags is respected and tag names are not mangled
version: ""
tags:
- name: WhatsApp Messages
description: Endpoints for managing WhatsApp messages
x-displayName: WhatsApp Messages
- name: PlantCare
description: Endpoints for plant care operations
paths:
/whatsapp/messages:
post:
tags:
- WhatsApp Messages
summary: Send Message
description: Send a WhatsApp message to a plant owner
operationId: whatsAppMessages_send
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/SendMessageRequest"
responses:
"200":
description: Successful Response
content:
application/json:
schema:
$ref: "#/components/schemas/Message"
x-fern-sdk-group-name: whatsAppMessages
x-fern-sdk-method-name: send
/whatsapp/messages/{messageId}:
get:
tags:
- WhatsApp Messages
summary: Get Message
description: Get a WhatsApp message by ID
operationId: whatsAppMessages_get
parameters:
- name: messageId
in: path
required: true
schema:
type: string
responses:
"200":
description: Successful Response
content:
application/json:
schema:
$ref: "#/components/schemas/Message"
x-fern-sdk-group-name: whatsAppMessages
x-fern-sdk-method-name: get
/plants/{plantId}/care:
post:
tags:
- PlantCare
summary: Water Plant
description: Water a plant
operationId: plantCare_water
parameters:
- name: plantId
in: path
required: true
schema:
type: string
responses:
"200":
description: Successful Response
content:
application/json:
schema:
$ref: "#/components/schemas/CareResult"
x-fern-sdk-group-name: plantCare
x-fern-sdk-method-name: water
components:
schemas:
SendMessageRequest:
type: object
properties:
to:
type: string
body:
type: string
required:
- to
- body
Message:
type: object
properties:
id:
type: string
to:
type: string
body:
type: string
status:
type: string
required:
- id
- to
- body
CareResult:
type: object
properties:
plantId:
type: string
action:
type: string
timestamp:
type: string
required:
- plantId
- action
Loading