From bcf9b47c507f50d1001b46c79298f2e2f22eef23 Mon Sep 17 00:00:00 2001 From: yogeshwaran-c Date: Wed, 29 Apr 2026 15:06:32 +0530 Subject: [PATCH] feat(oas2): support x-examples in body parameter request preview The OAS 2.0 `x-examples` vendor extension is a popular way to attach sample values to body parameters (used by tools such as swagger-jaxrs and io.swagger.models). Until now Swagger UI ignored those values entirely and rendered the auto-generated schema sample instead, leaving developers without an easy way to surface a realistic request body. When the parameter has no user-provided value, ParamBody now uses the `x-examples` "default" entry (or the first entry) as the initial body value, falling back to the generated sample when no `x-examples` are present. String examples are passed through verbatim; structured values are pretty-printed as JSON. Adds Jest coverage for ParamBody and a Cypress e2e fixture/spec that exercises the rendered example and the "Try it out" textarea for an OAS 2.0 spec with `x-examples`. Refs #3233 --- src/core/components/param-body.jsx | 28 +++- test/e2e-cypress/e2e/bugs/3233.cy.js | 25 +++ .../static/documents/bugs/3233.yaml | 28 ++++ test/unit/components/param-body.jsx | 150 ++++++++++++++++++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 test/e2e-cypress/e2e/bugs/3233.cy.js create mode 100644 test/e2e-cypress/static/documents/bugs/3233.yaml create mode 100644 test/unit/components/param-body.jsx diff --git a/src/core/components/param-body.jsx b/src/core/components/param-body.jsx index 878b4546ab4..23238db7448 100644 --- a/src/core/components/param-body.jsx +++ b/src/core/components/param-body.jsx @@ -57,7 +57,10 @@ export default class ParamBody extends PureComponent { this.setState({ value: val }) this.onChange(val, {isXml: isXml, isEditBox: isExecute}) } else { - if (isXml) { + let xExampleValue = this.xExampleValue() + if (xExampleValue !== undefined) { + this.onChange(xExampleValue, {isXml: isXml, isEditBox: isExecute}) + } else if (isXml) { this.onChange(this.sample("xml"), {isXml: isXml, isEditBox: isExecute}) } else { this.onChange(this.sample(), {isEditBox: isExecute}) @@ -65,6 +68,29 @@ export default class ParamBody extends PureComponent { } } + xExampleValue = () => { + const { param } = this.props + const xExamples = param.get("x-examples") + if (!xExamples || typeof xExamples.get !== "function" || xExamples.size === 0) { + return undefined + } + const defaultExample = xExamples.get("default") + const example = defaultExample !== undefined ? defaultExample : xExamples.first() + if (example === undefined) { + return undefined + } + if (typeof example === "string") { + return example + } + if (typeof example.toJS === "function") { + return JSON.stringify(example.toJS(), null, 2) + } + if (typeof example === "object") { + return JSON.stringify(example, null, 2) + } + return String(example) + } + sample = (xml) => { let { param, fn} = this.props let schema = fn.inferSchema(param.toJS()) diff --git a/test/e2e-cypress/e2e/bugs/3233.cy.js b/test/e2e-cypress/e2e/bugs/3233.cy.js new file mode 100644 index 00000000000..b881744e253 --- /dev/null +++ b/test/e2e-cypress/e2e/bugs/3233.cy.js @@ -0,0 +1,25 @@ +describe("#3233: x-examples should populate the body parameter example for OAS 2.0", () => { + it("renders the x-examples 'default' value as the request body example", () => { + cy.visit("?url=/documents/bugs/3233.yaml") + .get("#operations-default-dataTargets") + .click() + .get(".opblock-section") + .within(() => { + cy.get(".body-param__example") + .should("be.visible") + .should("include.text", "targets") + .should("include.text", "1") + .should("include.text", "4") + }) + }) + + it("populates the request body textarea with x-examples when 'Try it out' is enabled", () => { + cy.visit("?url=/documents/bugs/3233.yaml") + .get("#operations-default-dataTargets") + .click() + .get(".try-out__btn") + .click() + .get("textarea.body-param__text") + .should("contain.value", "targets") + }) +}) diff --git a/test/e2e-cypress/static/documents/bugs/3233.yaml b/test/e2e-cypress/static/documents/bugs/3233.yaml new file mode 100644 index 00000000000..30ff607da7e --- /dev/null +++ b/test/e2e-cypress/static/documents/bugs/3233.yaml @@ -0,0 +1,28 @@ +swagger: "2.0" +info: + description: "OAS 2.0 sample with x-examples in body parameter (issue #3233)" + version: "0.0.1" + title: "Swagger Sample" +paths: + /data/targets: + post: + summary: "Returns validation results of given list of targets." + description: "" + operationId: "dataTargets" + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + description: Targets list in JSON format. + required: true + schema: + type: object + x-examples: + default: + targets: [1, 2, 3, 4] + responses: + "200": + description: OK diff --git a/test/unit/components/param-body.jsx b/test/unit/components/param-body.jsx new file mode 100644 index 00000000000..31619e0bc37 --- /dev/null +++ b/test/unit/components/param-body.jsx @@ -0,0 +1,150 @@ +import React from "react" +import expect from "expect" +import { mount } from "enzyme" +import { fromJS } from "immutable" +import ParamBody from "core/components/param-body" + +describe("", () => { + const baseFn = { + inferSchema: () => ({}), + getSampleSchema: () => "GENERATED_SAMPLE", + } + + const baseSpecSelectors = { + parameterWithMetaByIdentity: (pathMethod, param) => param, + contentTypeValues: () => fromJS({ requestContentType: "application/json" }), + } + + const fakeGetComponent = () => () => null + + const createProps = (overrides = {}) => ({ + fn: baseFn, + getComponent: fakeGetComponent, + specSelectors: baseSpecSelectors, + pathMethod: ["/foo", "post"], + consumes: fromJS(["application/json"]), + consumesValue: "application/json", + isExecute: false, + onChange: jest.fn(), + onChangeConsumes: jest.fn(), + ...overrides, + }) + + it("emits the user-provided value when one is set", () => { + const onChange = jest.fn() + const props = createProps({ + param: fromJS({ + name: "body", + in: "body", + value: "{\"hello\":\"world\"}", + }), + onChange, + }) + + mount() + + expect(onChange).toHaveBeenCalledWith("{\"hello\":\"world\"}", false) + }) + + it("emits the generated sample when no value and no x-examples are present", () => { + const onChange = jest.fn() + const props = createProps({ + param: fromJS({ + name: "body", + in: "body", + schema: { type: "string" }, + }), + onChange, + }) + + mount() + + expect(onChange).toHaveBeenCalledWith("GENERATED_SAMPLE", undefined) + }) + + it("prefers the x-examples 'default' value over the generated sample", () => { + const onChange = jest.fn() + const props = createProps({ + param: fromJS({ + name: "body", + in: "body", + schema: { type: "object" }, + "x-examples": { + default: { targets: [1, 2, 3, 4] }, + }, + }), + onChange, + }) + + mount() + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith( + JSON.stringify({ targets: [1, 2, 3, 4] }, null, 2), + false + ) + }) + + it("falls back to the first x-examples entry when 'default' is missing", () => { + const onChange = jest.fn() + const props = createProps({ + param: fromJS({ + name: "body", + in: "body", + schema: { type: "object" }, + "x-examples": { + first: { foo: "bar" }, + second: { baz: "qux" }, + }, + }), + onChange, + }) + + mount() + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith( + JSON.stringify({ foo: "bar" }, null, 2), + false + ) + }) + + it("uses string x-examples values verbatim", () => { + const onChange = jest.fn() + const props = createProps({ + param: fromJS({ + name: "body", + in: "body", + schema: { type: "string" }, + "x-examples": { + default: "{\"targets\": \"[1, 2, 3, 4]\"}", + }, + }), + onChange, + }) + + mount() + + expect(onChange).toHaveBeenCalledWith( + "{\"targets\": \"[1, 2, 3, 4]\"}", + false + ) + }) + + it("ignores empty x-examples and falls back to the generated sample", () => { + const onChange = jest.fn() + const props = createProps({ + param: fromJS({ + name: "body", + in: "body", + schema: { type: "object" }, + "x-examples": {}, + }), + onChange, + }) + + mount() + + expect(onChange).toHaveBeenCalledWith("GENERATED_SAMPLE", undefined) + }) +})