diff --git a/scripts/build-api-data.js b/scripts/build-api-data.js
index b362d7de1..ff55d5dab 100644
--- a/scripts/build-api-data.js
+++ b/scripts/build-api-data.js
@@ -117,6 +117,75 @@ function categorizeParams(parameters) {
return { auth, pathParams, queryParams };
}
+// Local patches for known-wrong examples in upstream specs.
+// Each function takes the raw example and returns the corrected one.
+// Remove the matching entry once the upstream YAML is fixed.
+const REQUEST_BODY_EXAMPLE_OVERRIDES = {
+ // TE-17800: upstream uses `parent_folder_id`, real API expects `parent_id`.
+ // Rename both the key and its self-referential placeholder value.
+ 'Test Manager|POST|/api/v1/folder': (ex) => {
+ if (!ex || !Array.isArray(ex.folders)) return ex;
+ return {
+ ...ex,
+ folders: ex.folders.map((f) => {
+ if (!f || typeof f !== 'object' || !('parent_folder_id' in f)) return f;
+ const { parent_folder_id, ...rest } = f;
+ return { ...rest, parent_id: 'parent_id' };
+ }),
+ };
+ },
+ // PUT /api/v1/folder is now in REQUEST_BODY_VARIANTS_OVERRIDES below — the
+ // upstream example omits the `action` discriminator and conflates rename/move.
+};
+
+// Variant overrides expose multiple body shapes for one endpoint. The modal
+// renders a tab per variant; each variant has its own example + derived fields.
+// Use this when an endpoint accepts distinct payload shapes selected by a
+// discriminator (e.g. PUT /api/v1/folder uses `action: "update"` vs "move").
+const REQUEST_BODY_VARIANTS_OVERRIDES = {
+ // TE-17800: PUT /api/v1/folder serves two operations:
+ // - Rename: action="update", no parent_id/entity_type/serial_no
+ // - Move: action="move", adds parent_id + entity_type + serial_no
+ // The upstream YAML ships a single example that matches neither real payload.
+ 'Test Manager|PUT|/api/v1/folder': () => ([
+ {
+ name: 'Rename',
+ example: {
+ id: 'folder_id',
+ action: 'update',
+ entity_id: 'project_id',
+ name: 'Test Folder - LambdaTest Demo',
+ },
+ },
+ {
+ name: 'Move',
+ example: {
+ id: 'folder_id',
+ action: 'move',
+ entity_id: 'project_id',
+ entity_type: 'project',
+ name: 'Test Folder - LambdaTest Demo',
+ parent_id: 'parent_id',
+ serial_no: 1,
+ },
+ },
+ ]),
+};
+
+// Infer flat field metadata from a primitive-only example object — same logic
+// used by the legacy schema-less-fallback path, factored out for variant reuse.
+function inferFieldsFromExample(example) {
+ if (!example || typeof example !== 'object' || Array.isArray(example)) return [];
+ return Object.entries(example).map(([name, value]) => {
+ let type = 'string';
+ if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number';
+ else if (typeof value === 'boolean') type = 'boolean';
+ else if (Array.isArray(value)) type = 'array';
+ else if (typeof value === 'object' && value !== null) type = 'object';
+ return { name, type, required: false, description: '' };
+ });
+}
+
function resolveRef(ref, spec) {
if (!ref || !ref.startsWith('#/')) return null;
const parts = ref.slice(2).split('/');
@@ -145,6 +214,15 @@ function extractRequestBody(requestBody, spec, ctx = {}) {
}
schema = merged;
}
+ // Resolve the example up front (with local override) so derived property names
+ // from the example fallback below also pick up the correction.
+ const rawExample = bodyContent.example ?? schema.example;
+ const overrideKey = (ctx.specName && ctx.method && ctx.path)
+ ? `${ctx.specName}|${ctx.method}|${ctx.path}`
+ : null;
+ const overrideFn = overrideKey ? REQUEST_BODY_EXAMPLE_OVERRIDES[overrideKey] : null;
+ const exampleValue = overrideFn ? overrideFn(rawExample) : rawExample;
+
const props = schema.properties || {};
const required = schema.required || [];
let properties = Object.entries(props).map(([name, propSchema]) => {
@@ -161,25 +239,38 @@ function extractRequestBody(requestBody, spec, ctx = {}) {
// editable inputs. Inferred types and required flags are best-effort;
// long-term fix is for spec authors to add proper `properties`.
if (properties.length === 0) {
- const example = bodyContent.example ?? schema.example;
- if (example && typeof example === 'object' && !Array.isArray(example)) {
- properties = Object.entries(example).map(([name, value]) => {
- let type = 'string';
- if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number';
- else if (typeof value === 'boolean') type = 'boolean';
- else if (Array.isArray(value)) type = 'array';
- else if (typeof value === 'object' && value !== null) type = 'object';
- return { name, type, required: false, description: '' };
- });
- if (properties.length > 0) {
- const loc = ctx.method && ctx.path ? `${ctx.method} ${ctx.path}` : '(unknown endpoint)';
- const src = ctx.specName ? ` [${ctx.specName}]` : '';
- console.warn(`⚠️ ${loc}${src}: requestBody has no schema.properties; derived ${properties.length} field(s) from example. Spec should declare properties for accurate types/required/descriptions.`);
- }
+ properties = inferFieldsFromExample(exampleValue);
+ if (properties.length > 0) {
+ const loc = ctx.method && ctx.path ? `${ctx.method} ${ctx.path}` : '(unknown endpoint)';
+ const src = ctx.specName ? ` [${ctx.specName}]` : '';
+ console.warn(`⚠️ ${loc}${src}: requestBody has no schema.properties; derived ${properties.length} field(s) from example. Spec should declare properties for accurate types/required/descriptions.`);
}
}
- return { contentType, description: requestBody.description || '', properties };
+ // Variant overrides: attach per-tab examples + derived fields when an
+ // endpoint serves multiple payload shapes (e.g. PUT folder rename vs move).
+ const variantsFn = overrideKey ? REQUEST_BODY_VARIANTS_OVERRIDES[overrideKey] : null;
+ const variants = variantsFn
+ ? variantsFn().map((v) => ({
+ name: v.name,
+ example: v.example,
+ properties: inferFieldsFromExample(v.example),
+ }))
+ : null;
+
+ // When variants are present, the first one becomes the canonical top-level
+ // example + properties so non-variant-aware consumers (e.g. the static
+ // CodeExamples panel) show a sensible default instead of stale spec data.
+ const finalExample = variants && variants.length > 0 ? variants[0].example : exampleValue;
+ const finalProperties = variants && variants.length > 0 ? variants[0].properties : properties;
+
+ return {
+ contentType,
+ description: requestBody.description || '',
+ properties: finalProperties,
+ ...(finalExample !== undefined && { example: finalExample }),
+ ...(variants && variants.length > 0 && { variants }),
+ };
}
function schemaToExample(schema, spec, seen = new Set()) {
diff --git a/src/component/ApiReference/CodeExamples.js b/src/component/ApiReference/CodeExamples.js
index d2a82dd19..7edef4944 100644
--- a/src/component/ApiReference/CodeExamples.js
+++ b/src/component/ApiReference/CodeExamples.js
@@ -74,6 +74,57 @@ function CodeHighlight({ code, language }) {
);
}
+// One stacked code panel — title bar with copy button (and optional language
+// selector on the first panel of a multi-variant code section), code body below.
+function CodePanel({
+ title, code, language,
+ showLangSelector, selectedLang, setSelectedLang,
+ langDropdownOpen, setLangDropdownOpen, closeLangDropdown, langBtnRef,
+ onCopy,
+}) {
+ const [copied, setCopied] = useState(false);
+ function handleCopy() {
+ onCopy(setCopied);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ }
+ return (
+
+
+ {title}
+ {showLangSelector && (
+ <>
+ setLangDropdownOpen((o) => !o)}
+ btnRef={langBtnRef}
+ />
+
+ >
+ )}
+
+
+
+
+
+
+ );
+}
+
function formatResponse(value) {
if (value == null) return '';
if (typeof value === 'string') {
@@ -99,8 +150,28 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp,
if (!endpoint) return null;
+ // When the endpoint ships requestBody.variants (e.g. PUT folder rename/move),
+ // render one code panel per variant stacked vertically. Each panel gets the
+ // variant's example + properties as a shimmed endpoint so the snippet matches
+ // that payload shape. Plain endpoints render a single panel as before.
+ const variants = endpoint.requestBody?.variants;
+ const summary = endpoint.name || endpoint.summary || '';
+ const codePanels = (variants && variants.length > 0)
+ ? variants.map((v) => ({
+ title: `${summary} (${v.name})`,
+ code: generateCodeExample(
+ {
+ ...endpoint,
+ requestBody: { ...endpoint.requestBody, properties: v.properties, example: v.example },
+ },
+ selectedLang,
+ ),
+ }))
+ : [{ title: endpoint.name, code: generateCodeExample(endpoint, selectedLang) }];
const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0];
- const code = generateCodeExample(endpoint, selectedLang);
+ // Keep `code` available for the existing single-pane copy handler (used by
+ // the first panel's copy button when there are no variants).
+ const code = codePanels[0].code;
const responses = endpoint.responses || {};
const responseTabs = Object.keys(responses).filter(
@@ -129,36 +200,25 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp,
return (
- {/* Code Examples Panel */}
-
-
- {endpoint.name}
- setLangDropdownOpen((o) => !o)}
- btnRef={langBtnRef}
- />
-
-
-
-
-
-
-
+ {/* Code Examples Panel(s). When the endpoint has variants we render one
+ panel per variant, stacked vertically. The language selector lives in
+ the first panel's header and applies to all panels. */}
+ {codePanels.map((panel, idx) => (
+
copyText(panel.code, setCopied)}
+ />
+ ))}
{/* Response Panel */}
{responseTabs.length > 0 && (
diff --git a/src/component/ApiReference/EndpointDetail.js b/src/component/ApiReference/EndpointDetail.js
index 1ccce20af..d493dd77d 100644
--- a/src/component/ApiReference/EndpointDetail.js
+++ b/src/component/ApiReference/EndpointDetail.js
@@ -764,23 +764,38 @@ export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeS
)}
- {/* Request Body */}
- {endpoint.requestBody && Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0 && (
-
-
-
Body
- {endpoint.requestBody.contentType && (
-
{endpoint.requestBody.contentType}
+ {/* Request Body — one section per variant when present, otherwise the
+ plain body. Variants stack vertically so users see all payload shapes
+ (e.g. PUT folder rename/move) without a tab interaction. */}
+ {endpoint.requestBody && (() => {
+ const variants = endpoint.requestBody.variants;
+ const summary = endpoint.name || endpoint.summary || 'Body';
+ const blocks = (variants && variants.length > 0)
+ ? variants.map((v) => ({
+ title: `${summary} (${v.name})`,
+ props: v.properties,
+ }))
+ : (Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0
+ ? [{ title: 'Body', props: endpoint.requestBody.properties }]
+ : []);
+ if (blocks.length === 0) return null;
+ return blocks.map((block) => (
+
+
+
{block.title}
+ {endpoint.requestBody.contentType && (
+ {endpoint.requestBody.contentType}
+ )}
+
+ {endpoint.requestBody.description && (
+
)}
-
- {endpoint.requestBody.description && (
-
- )}
- {endpoint.requestBody.properties.map((prop, i) => (
-
- ))}
-
- )}
+ {block.props.map((prop, i) => (
+
+ ))}
+
+ ));
+ })()}
{/* Response section */}
diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js
index 933a59ed0..60c8a394b 100644
--- a/src/component/ApiReference/TryItModal.js
+++ b/src/component/ApiReference/TryItModal.js
@@ -4,7 +4,7 @@ import { Highlight, themes } from 'prism-react-renderer';
import MethodBadge from './MethodBadge';
import InlineText from './InlineText';
import styles from './TryItModal.module.css';
-import { LANGUAGES, generateCodeExample, LangDropdownPortal, LangSelectorButton, coerceBodyValue } from './langUtils';
+import { LANGUAGES, generateCodeExample, LangDropdownPortal, LangSelectorButton, coerceBodyValue, detectFlattenedArrayBody } from './langUtils';
const githubWithGreenKeys = {
...themes.github,
@@ -94,19 +94,58 @@ function buildCurl(endpoint, username, password, params, baseUrl) {
const authLine = hasAuth ? ` \\\n --header 'Authorization: ${authHeader}'` : '';
const bodyProps = endpoint.requestBody?.properties || [];
+ const rawExample = endpoint.requestBody?.example;
const contentType = endpoint.requestBody?.contentType || 'application/json';
+ const isMultipart = contentType === 'multipart/form-data';
+ const flattenedBody = !isMultipart ? detectFlattenedArrayBody(endpoint) : null;
+ const userFilledBody = flattenedBody
+ ? flattenedBody.innerFields.some((f) => params[`__body__${f.name}`])
+ : bodyProps.some((p) => params[`__body__${p.name}`]);
+ const useRawExample = !isMultipart && !flattenedBody && !userFilledBody
+ && rawExample != null && typeof rawExample === 'object';
let bodyLine = '';
- if (bodyProps.length > 0) {
- const bodyEntries = bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]);
- if (contentType === 'multipart/form-data') {
+ if (bodyProps.length > 0 || useRawExample || flattenedBody) {
+ if (isMultipart) {
+ const bodyEntries = bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]);
bodyLine = bodyEntries
.filter(([, v]) => v)
.map(([k, v]) => ` \\\n --form '${k}=${v}'`)
.join('');
+ } else if (flattenedBody) {
+ // Assemble inner object from per-field inputs (with example fallback),
+ // wrap under the spec's array key.
+ const inner = {};
+ for (const f of flattenedBody.innerFields) {
+ const raw = params[`__body__${f.name}`];
+ const fromEx = flattenedBody.innerExample[f.name];
+ const val = (raw !== undefined && raw !== '')
+ ? coerceBodyValue(raw, f.type)
+ : fromEx;
+ if (val !== undefined && val !== '') inner[f.name] = val;
+ }
+ const bodyJson = { [flattenedBody.wrapperKey]: [inner] };
+ bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyJson, null, 2)}'`;
} else {
- const bodyObj = Object.fromEntries(bodyEntries.filter(([, v]) => v));
- if (Object.keys(bodyObj).length) {
- bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyObj)}'`;
+ let bodyJson;
+ if (useRawExample) {
+ bodyJson = rawExample;
+ } else {
+ // Empty inputs fall back to the spec example so editing one field
+ // doesn't blank out the others.
+ const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined;
+ const bodyEntries = bodyProps.map((p) => {
+ const raw = params[`__body__${p.name}`] || '';
+ if (!raw) {
+ const ex = fromExample(p);
+ return [p.name, ex !== undefined ? ex : ''];
+ }
+ return [p.name, coerceBodyValue(raw, p.type)];
+ });
+ const bodyObj = Object.fromEntries(bodyEntries.filter(([, v]) => v !== '' && v !== undefined));
+ if (Object.keys(bodyObj).length) bodyJson = bodyObj;
+ }
+ if (bodyJson !== undefined) {
+ bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyJson, null, 2)}'`;
}
}
}
@@ -284,7 +323,69 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
- const [params, setParams] = useState({});
+ // Variant-aware view of the request body: when the spec ships multiple
+ // payload shapes for one endpoint (e.g. PUT folder rename/move), the modal
+ // picks one tab and presents it as the effective body. Downstream helpers
+ // (buildCurl, handleSend, generateCodeExample) operate on `effectiveEndpoint`
+ // and stay variant-agnostic.
+ const variants = endpoint.requestBody?.variants || null;
+ const [selectedVariantIdx, setSelectedVariantIdx] = useState(0);
+ const selectedVariant = variants ? variants[selectedVariantIdx] : null;
+ const effectiveEndpoint = useMemo(() => {
+ if (!selectedVariant) return endpoint;
+ return {
+ ...endpoint,
+ requestBody: {
+ ...endpoint.requestBody,
+ properties: selectedVariant.properties,
+ example: selectedVariant.example,
+ },
+ };
+ }, [endpoint, selectedVariant]);
+ // Compute once at render time — both the params initializer and the body
+ // section need it, and `effectiveEndpoint` is stable for the variant's lifetime.
+ const flattenedBody = detectFlattenedArrayBody(effectiveEndpoint);
+
+ // Build the per-variant prefill — also reused when the user switches tabs.
+ function computeInitialParams() {
+ const initial = {};
+ if (flattenedBody) {
+ for (const f of flattenedBody.innerFields) {
+ const val = flattenedBody.innerExample[f.name];
+ initial[`__body__${f.name}`] = val == null ? '' : String(val);
+ }
+ return initial;
+ }
+ const ex = effectiveEndpoint.requestBody?.example;
+ const props = effectiveEndpoint.requestBody?.properties || [];
+ if (ex && typeof ex === 'object' && !Array.isArray(ex)) {
+ for (const p of props) {
+ if (ex[p.name] === undefined) continue;
+ const v = ex[p.name];
+ initial[`__body__${p.name}`] = v == null ? '' : String(v);
+ }
+ }
+ return initial;
+ }
+ // Prefill body fields with the spec example so the form is usable on first
+ // open. Flattened-array bodies expose inner primitive fields; plain bodies
+ // get one prefilled input per property.
+ const [params, setParams] = useState(computeInitialParams);
+
+ // Reset & re-prefill body params whenever the active variant changes.
+ // Other params (path/query/auth) are not affected by variant switches.
+ useEffect(() => {
+ if (!variants) return;
+ setParams((prev) => {
+ const next = {};
+ for (const k of Object.keys(prev)) {
+ if (!k.startsWith('__body__')) next[k] = prev[k];
+ }
+ const fresh = computeInitialParams();
+ return { ...next, ...fresh };
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedVariantIdx]);
const [response, setResponse] = useState(null);
const [loading, setLoading] = useState(false);
const [activeResTab, setActiveResTab] = useState(null);
@@ -347,18 +448,53 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
? `Basic ${safeBase64(`${username}:${password}`)}`
: '';
- const bodyProps = endpoint.requestBody?.properties || [];
- const contentType = endpoint.requestBody?.contentType || 'application/json';
+ const bodyProps = effectiveEndpoint.requestBody?.properties || [];
+ const rawExample = effectiveEndpoint.requestBody?.example;
+ const contentType = effectiveEndpoint.requestBody?.contentType || 'application/json';
+ const isMultipart = contentType === 'multipart/form-data';
+ const flattenedBodyHS = !isMultipart ? detectFlattenedArrayBody(effectiveEndpoint) : null;
+ const userFilledBody = flattenedBodyHS
+ ? flattenedBodyHS.innerFields.some((f) => params[`__body__${f.name}`])
+ : bodyProps.some((p) => params[`__body__${p.name}`]);
+ const useRawExample = !isMultipart && !flattenedBodyHS && !userFilledBody
+ && rawExample != null && typeof rawExample === 'object';
let fetchBody;
let fetchHeaders = { ...(authHeader && { Authorization: authHeader }) };
- if (bodyProps.length > 0) {
- if (contentType === 'multipart/form-data') {
+ if (bodyProps.length > 0 || useRawExample || flattenedBodyHS) {
+ if (isMultipart) {
const fd = new FormData();
bodyProps.forEach((p) => { if (params[`__body__${p.name}`]) fd.append(p.name, params[`__body__${p.name}`]); });
fetchBody = fd;
// Don't set Content-Type for FormData — browser sets it with boundary
+ } else if (flattenedBodyHS) {
+ const inner = {};
+ for (const f of flattenedBodyHS.innerFields) {
+ const raw = params[`__body__${f.name}`];
+ const fromEx = flattenedBodyHS.innerExample[f.name];
+ const val = (raw !== undefined && raw !== '')
+ ? coerceBodyValue(raw, f.type)
+ : fromEx;
+ if (val !== undefined && val !== '') inner[f.name] = val;
+ }
+ fetchBody = JSON.stringify({ [flattenedBodyHS.wrapperKey]: [inner] });
+ fetchHeaders['Content-Type'] = 'application/json';
+ } else if (useRawExample) {
+ fetchBody = JSON.stringify(rawExample);
+ fetchHeaders['Content-Type'] = 'application/json';
} else {
- const bodyObj = Object.fromEntries(bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]).filter(([, v]) => v));
+ // Empty inputs fall back to the spec example so partial edits don't
+ // strip the other example values.
+ const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined;
+ const bodyObj = Object.fromEntries(
+ bodyProps.map((p) => {
+ const raw = params[`__body__${p.name}`] || '';
+ if (!raw) {
+ const ex = fromExample(p);
+ return [p.name, ex !== undefined ? ex : ''];
+ }
+ return [p.name, coerceBodyValue(raw, p.type)];
+ }).filter(([, v]) => v !== '' && v !== undefined)
+ );
if (Object.keys(bodyObj).length) {
fetchBody = JSON.stringify(bodyObj);
fetchHeaders['Content-Type'] = 'application/json';
@@ -384,16 +520,16 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
}
}
- const curlCode = buildCurl(endpoint, username, password, params, selectedServer);
+ const curlCode = buildCurl(effectiveEndpoint, username, password, params, selectedServer);
const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0];
const codeToShow = selectedLang === 'cURL'
? curlCode
- : generateCodeExample(endpoint, selectedLang, { username, password, params });
+ : generateCodeExample(effectiveEndpoint, selectedLang, { username, password, params });
const hasAuth = endpoint.auth && endpoint.auth.length > 0;
const hasQuery = endpoint.queryParams && endpoint.queryParams.length > 0;
const hasPath = endpoint.pathParams && endpoint.pathParams.length > 0;
- const bodyProps = endpoint.requestBody?.properties || [];
- const hasBody = bodyProps.length > 0;
+ const bodyProps = effectiveEndpoint.requestBody?.properties || [];
+ const hasBody = bodyProps.length > 0 || !!variants;
// Static spec responses — always shown
const specResponses = endpoint.responses || {};
@@ -548,18 +684,56 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
{hasBody && (
- {bodyProps.map((p) => (
- updateParam(`__body__${p.name}`, v)}
- placeholder={(p.type || '').toLowerCase().includes('array') ? 'e.g. ["val1", "val2"] or val1, val2' : undefined}
- />
- ))}
+ {variants && (
+
+ {variants.map((v, idx) => {
+ const active = idx === selectedVariantIdx;
+ return (
+
+ );
+ })}
+
+ )}
+ {flattenedBody ? (
+ flattenedBody.innerFields.map((f) => (
+ updateParam(`__body__${f.name}`, v)}
+ />
+ ))
+ ) : (
+ bodyProps.map((p) => (
+ updateParam(`__body__${p.name}`, v)}
+ placeholder={(p.type || '').toLowerCase().includes('array') ? 'e.g. ["val1", "val2"] or val1, val2' : undefined}
+ />
+ ))
+ )}
)}
diff --git a/src/component/ApiReference/langUtils.js b/src/component/ApiReference/langUtils.js
index 11255fdb9..fa5ef433f 100644
--- a/src/component/ApiReference/langUtils.js
+++ b/src/component/ApiReference/langUtils.js
@@ -38,6 +38,35 @@ export function coerceBodyValue(raw, type) {
return raw;
}
+// Detect bodies shaped like `{ wrapper: [ { ...primitive fields } ] }` (e.g.
+// Test Manager Create Folder). When matched, the form can render the inner
+// object's primitive fields directly and the body assembly wraps them back
+// into the array shape — giving the same per-field UX as a flat-body endpoint.
+export function detectFlattenedArrayBody(endpoint) {
+ if (!endpoint || !endpoint.requestBody) return null;
+ const props = endpoint.requestBody.properties || [];
+ if (props.length !== 1) return null;
+ const only = props[0];
+ if (only.type !== 'array') return null;
+ const example = endpoint.requestBody.example;
+ if (!example || typeof example !== 'object' || Array.isArray(example)) return null;
+ const arr = example[only.name];
+ if (!Array.isArray(arr) || arr.length !== 1) return null;
+ const inner = arr[0];
+ if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null;
+ // Bail when any inner value is an array or non-null object — the flat form
+ // can't represent nesting. `null` values are accepted (typed as string).
+ const innerFields = Object.entries(inner).map(([name, value]) => {
+ let type = 'string';
+ if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number';
+ else if (typeof value === 'boolean') type = 'boolean';
+ else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) return null;
+ return { name, type, required: false, description: '' };
+ });
+ if (innerFields.some((f) => f === null)) return null;
+ return { wrapperKey: only.name, innerFields, innerExample: inner };
+}
+
// prism language mapping — only use languages bundled in prism-react-renderer
export const LANGUAGES = [
{ label: 'cURL', prism: 'clike' },
@@ -75,21 +104,52 @@ export function generateCodeExample(endpoint, language, { username, password, pa
const bodyProps = endpoint.requestBody?.properties || [];
const contentType = endpoint.requestBody?.contentType || 'application/json';
const isMultipart = contentType === 'multipart/form-data';
- const bodyExample = bodyProps.length > 0
- ? Object.fromEntries(bodyProps.map((p) => {
- const raw = params && params[`__body__${p.name}`];
- let val;
- if (raw) {
- val = coerceBodyValue(raw, p.type);
- } else {
- val = p.type.includes('integer') || p.type.includes('number') ? 0 :
- p.type.includes('boolean') ? true :
- p.type.includes('array') ? [] :
- `<${p.name}>`;
- }
- return [p.name, val];
- }))
- : null;
+ const rawExample = endpoint.requestBody?.example;
+ const flattenedBody = !isMultipart ? detectFlattenedArrayBody(endpoint) : null;
+ const userFilledBody = flattenedBody
+ ? flattenedBody.innerFields.some((f) => params && params[`__body__${f.name}`])
+ : bodyProps.some((p) => params && params[`__body__${p.name}`]);
+ // Use the spec's example block as the body when the user hasn't typed
+ // anything — keeps nested arrays/objects that property-based synthesis
+ // would flatten to `[]` (e.g. Test Manager Create Folder's `folders[]`).
+ const useRawExample = !isMultipart && !flattenedBody && !userFilledBody
+ && rawExample != null && typeof rawExample === 'object';
+ let bodyExample;
+ if (flattenedBody) {
+ // Assemble the inner object from per-field inputs (with example fallback),
+ // wrap under the spec's array key so the snippet matches the spec shape.
+ const inner = {};
+ for (const f of flattenedBody.innerFields) {
+ const raw = params && params[`__body__${f.name}`];
+ const fromEx = flattenedBody.innerExample[f.name];
+ const val = (raw !== undefined && raw !== '')
+ ? coerceBodyValue(raw, f.type)
+ : fromEx;
+ if (val !== undefined && val !== '') inner[f.name] = val;
+ }
+ bodyExample = { [flattenedBody.wrapperKey]: [inner] };
+ } else if (useRawExample) {
+ bodyExample = rawExample;
+ } else if (bodyProps.length > 0) {
+ bodyExample = Object.fromEntries(bodyProps.map((p) => {
+ const raw = params && params[`__body__${p.name}`];
+ const fromExample = rawExample && typeof rawExample === 'object' ? rawExample[p.name] : undefined;
+ let val;
+ if (raw) {
+ val = coerceBodyValue(raw, p.type);
+ } else if (fromExample !== undefined) {
+ val = fromExample;
+ } else {
+ val = p.type.includes('integer') || p.type.includes('number') ? 0 :
+ p.type.includes('boolean') ? true :
+ p.type.includes('array') ? [] :
+ `<${p.name}>`;
+ }
+ return [p.name, val];
+ }));
+ } else {
+ bodyExample = null;
+ }
switch (language) {
case 'cURL': {