Skip to content

Commit 47258df

Browse files
authored
fix: generate correct examples for different request content types (#1283) (#1284)
Code examples were showing incorrect request body parameters when switching between content types (e.g., JSON vs XML). This occurred because a single example was generated at build time and reused for all content types. Changes: - Dynamically generate request examples based on the current content type's schema - Update body in Redux state when content type changes - Add lazy loading to MimeTabs in RequestSchema to prevent rendering all schemas - Force LiveApp components to remount when content type changes using key prop - Add test case with different schemas for JSON and XML content types Fixes #1283
1 parent 150c61d commit 47258df

File tree

3 files changed

+123
-44
lines changed

3 files changed

+123
-44
lines changed

demo/examples/tests/examples.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,3 +549,56 @@ paths:
549549
examples:
550550
- true
551551
- false
552+
553+
/requestBody/multipleContentTypes:
554+
post:
555+
tags:
556+
- examples
557+
summary: multiple content types with different schemas
558+
description: "This endpoint accepts either JSON or XML with different schemas"
559+
security:
560+
- BearerAuth: []
561+
requestBody:
562+
description: Example endpoint request
563+
required: true
564+
content:
565+
application/json:
566+
schema:
567+
type: object
568+
required:
569+
- example_request_json_param
570+
properties:
571+
example_request_json_param:
572+
type: string
573+
description: This field should only appear in JSON requests
574+
example: "json_value"
575+
application/xml:
576+
schema:
577+
type: object
578+
required:
579+
- example_request_xml_param
580+
properties:
581+
example_request_xml_param:
582+
type: string
583+
description: This field should only appear in XML requests
584+
example: "xml_value"
585+
responses:
586+
"200":
587+
description: Successful operation
588+
content:
589+
application/json:
590+
schema:
591+
type: object
592+
required:
593+
- example_response_param
594+
properties:
595+
example_response_param:
596+
type: string
597+
description: The example response parameter
598+
example: "response_value"
599+
components:
600+
securitySchemes:
601+
BearerAuth:
602+
type: http
603+
scheme: bearer
604+
bearerFormat: JWT

packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
* ========================================================================== */
77

8-
import React from "react";
8+
import React, { useEffect } from "react";
99

1010
import { translate } from "@docusaurus/Translate";
1111

@@ -19,6 +19,7 @@ import SchemaTabs from "@theme/SchemaTabs";
1919
import TabItem from "@theme/TabItem";
2020
import { OPENAPI_BODY, OPENAPI_REQUEST } from "@theme/translationIds";
2121
import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
22+
import { sampleFromSchema } from "docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample";
2223
import format from "xml-formatter";
2324

2425
import { clearRawBody, setFileRawBody, setStringRawBody } from "./slice";
@@ -149,12 +150,21 @@ function Body({
149150
let exampleBody;
150151
let examplesBodies = [] as any;
151152

153+
// Generate example from the schema for the current content type
154+
let contentTypeExample;
155+
if (schema) {
156+
contentTypeExample = sampleFromSchema(schema, { type: "request" });
157+
} else if (jsonRequestBodyExample) {
158+
// Fallback to the build-time generated example if no schema is available
159+
contentTypeExample = jsonRequestBodyExample;
160+
}
161+
152162
if (
153163
contentType.includes("application/json") ||
154164
contentType.endsWith("+json")
155165
) {
156-
if (jsonRequestBodyExample) {
157-
defaultBody = JSON.stringify(jsonRequestBodyExample, null, 2);
166+
if (contentTypeExample) {
167+
defaultBody = JSON.stringify(contentTypeExample, null, 2);
158168
}
159169
if (example) {
160170
exampleBody = JSON.stringify(example, null, 2);
@@ -191,15 +201,15 @@ function Body({
191201
}
192202

193203
if (contentType === "application/xml" || contentType.endsWith("+xml")) {
194-
if (jsonRequestBodyExample) {
204+
if (contentTypeExample) {
195205
try {
196-
defaultBody = format(json2xml(jsonRequestBodyExample, ""), {
206+
defaultBody = format(json2xml(contentTypeExample, ""), {
197207
indentation: " ",
198208
lineSeparator: "\n",
199209
collapseContent: true,
200210
});
201211
} catch {
202-
defaultBody = json2xml(jsonRequestBodyExample);
212+
defaultBody = json2xml(contentTypeExample);
203213
}
204214
}
205215
if (example) {
@@ -255,6 +265,15 @@ function Body({
255265
language = "xml";
256266
}
257267

268+
// Update body in Redux when content type changes
269+
useEffect(() => {
270+
if (defaultBody) {
271+
dispatch(setStringRawBody(defaultBody));
272+
}
273+
// Only re-run when contentType changes, not when defaultBody changes
274+
// eslint-disable-next-line react-hooks/exhaustive-deps
275+
}, [contentType]);
276+
258277
if (exampleBody) {
259278
return (
260279
<FormItem>
@@ -269,6 +288,7 @@ function Body({
269288
default
270289
>
271290
<LiveApp
291+
key={contentType}
272292
action={(code: string) => dispatch(setStringRawBody(code))}
273293
language={language}
274294
required={required}
@@ -281,6 +301,7 @@ function Body({
281301
{example.summary && <Markdown>{example.summary}</Markdown>}
282302
{exampleBody && (
283303
<LiveApp
304+
key={contentType}
284305
action={(code: string) => dispatch(setStringRawBody(code))}
285306
language={language}
286307
required={required}
@@ -308,6 +329,7 @@ function Body({
308329
default
309330
>
310331
<LiveApp
332+
key={contentType}
311333
action={(code: string) => dispatch(setStringRawBody(code))}
312334
language={language}
313335
required={required}
@@ -326,6 +348,7 @@ function Body({
326348
{example.summary && <Markdown>{example.summary}</Markdown>}
327349
{example.body && (
328350
<LiveApp
351+
key={`${contentType}-${example.label}`}
329352
action={(code: string) => dispatch(setStringRawBody(code))}
330353
language={language}
331354
>
@@ -343,6 +366,7 @@ function Body({
343366
return (
344367
<FormItem>
345368
<LiveApp
369+
key={contentType}
346370
action={(code: string) => dispatch(setStringRawBody(code))}
347371
language={language}
348372
required={required}

packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
4545

4646
if (mimeTypes.length > 1) {
4747
return (
48-
<MimeTabs className="openapi-tabs__mime" schemaType="request">
48+
<MimeTabs className="openapi-tabs__mime" schemaType="request" lazy>
4949
{mimeTypes.map((mimeType) => {
5050
const firstBody = body.content![mimeType].schema;
5151
if (
@@ -58,43 +58,45 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
5858
return (
5959
// @ts-ignore
6060
<TabItem key={mimeType} label={mimeType} value={mimeType}>
61-
<Details
62-
className="openapi-markdown__details mime"
63-
data-collapsed={false}
64-
open={true}
65-
style={style}
66-
summary={
67-
<>
68-
<summary>
69-
<h3 className="openapi-markdown__details-summary-header-body">
70-
{translate({
71-
id: OPENAPI_REQUEST.BODY_TITLE,
72-
message: title,
73-
})}
74-
{body.required === true && (
75-
<span className="openapi-schema__required">
76-
{translate({
77-
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
78-
message: "required",
79-
})}
80-
</span>
81-
)}
82-
</h3>
83-
</summary>
84-
</>
85-
}
86-
>
87-
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
88-
{body.description && (
89-
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
90-
<Markdown>{body.description}</Markdown>
91-
</div>
92-
)}
93-
</div>
94-
<ul style={{ marginLeft: "1rem" }}>
95-
<SchemaNode schema={firstBody} schemaType="request" />
96-
</ul>
97-
</Details>
61+
<div style={{ marginTop: "1rem" }}>
62+
<Details
63+
className="openapi-markdown__details mime"
64+
data-collapsed={false}
65+
open={true}
66+
style={style}
67+
summary={
68+
<>
69+
<summary>
70+
<h3 className="openapi-markdown__details-summary-header-body">
71+
{translate({
72+
id: OPENAPI_REQUEST.BODY_TITLE,
73+
message: title,
74+
})}
75+
{body.required === true && (
76+
<span className="openapi-schema__required">
77+
{translate({
78+
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
79+
message: "required",
80+
})}
81+
</span>
82+
)}
83+
</h3>
84+
</summary>
85+
</>
86+
}
87+
>
88+
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
89+
{body.description && (
90+
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
91+
<Markdown>{body.description}</Markdown>
92+
</div>
93+
)}
94+
</div>
95+
<ul style={{ marginLeft: "1rem" }}>
96+
<SchemaNode schema={firstBody} schemaType="request" />
97+
</ul>
98+
</Details>
99+
</div>
98100
</TabItem>
99101
);
100102
})}

0 commit comments

Comments
 (0)