From be00d6da6f6f9b8e5140bd5da1529e7110f88482 Mon Sep 17 00:00:00 2001 From: adrianodpdiaz Date: Sat, 4 Apr 2026 20:16:57 +0200 Subject: [PATCH 1/2] feat(oas3): auto-generated examples for requestBody using oneOf (#10765) --- .../plugins/oas3/components/request-body.jsx | 80 +++++- test/unit/core/plugins/oas3/request-body.js | 237 ++++++++++++++++++ 2 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 test/unit/core/plugins/oas3/request-body.js diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 388f5cc6662..1197ca21a9c 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -30,6 +30,57 @@ export const getDefaultRequestBodyValue = (requestBody, mediaType, activeExample return stringify(exampleValue) } +/** + * When a schema uses `oneOf` or `anyOf` and the media type has no explicit + * `examples` object, synthesize one example entry per subschema so that the + * existing ExamplesSelect dropdown can be used to switch between them. + * + * Returns an Immutable OrderedMap shaped like the `examples` object from the + * OpenAPI spec (each entry has at least a `summary` and a `value`), or `null` + * when the schema does not use `oneOf`/`anyOf`. + */ +export const getOneOfExamples = (requestBody, mediaType, fn) => { + const mediaTypeValue = requestBody.getIn(["content", mediaType]) ?? OrderedMap() + + const hasExamplesKey = mediaTypeValue.get("examples") !== undefined + if (hasExamplesKey) + return null + + const schema = mediaTypeValue.get("schema", OrderedMap()) + const schemaJS = typeof schema.toJS === "function" ? schema.toJS() : schema + + const variants = schemaJS.oneOf ?? schemaJS.anyOf ?? null + if (!Array.isArray(variants) || variants.length < 2) + return null + + const keyword = schemaJS.oneOf ? "oneOf" : "anyOf" + + // Build a parent schema that omits the oneOf/anyOf key so that + // getSampleSchema generates a sample for each individual variant. + const { [keyword]: _omitted, ...parentWithoutVariants } = schemaJS + + const entries = variants.map((variant, index) => { + const variantSchema = fn.mergeJsonSchema + ? fn.mergeJsonSchema(parentWithoutVariants, variant) + : variant + + const nameFromRef = variant["$$ref"] + ?.replace(/^.*#\/components\/schemas\//, "") + ?.replace(/^.*#\/definitions\//, "") + const summary = + variant.title ?? variant["x-summary"] ?? nameFromRef ?? `${keyword}[${index}]` + const value = fn.getSampleSchema( + fromJS(variantSchema), + mediaType, + { includeWriteOnly: true } + ) + + return [summary, Map({ summary, value: stringify(value) })] + }) + + return OrderedMap(entries) +} + const RequestBody = ({ @@ -85,18 +136,23 @@ const RequestBody = ({ const mediaTypeValue = requestBodyContent.get(contentType) ?? OrderedMap() const schemaForMediaType = mediaTypeValue.get("schema", OrderedMap()) const rawExamplesOfMediaType = mediaTypeValue.get("examples", null) - const sampleForMediaType = rawExamplesOfMediaType?.map((container, key) => { - const val = container?.get("value", null) - if(val) { - container = container.set("value", getDefaultRequestBodyValue( - requestBody, - contentType, - key, - fn, - ), val) - } - return container - }) + const oneOfExamples = rawExamplesOfMediaType + ? null + : getOneOfExamples(requestBody, contentType, fn) + const sampleForMediaType = rawExamplesOfMediaType + ? rawExamplesOfMediaType.map((container, key) => { + const val = container?.get("value", null) + if(val) { + container = container.set("value", getDefaultRequestBodyValue( + requestBody, + contentType, + key, + fn, + ), val) + } + return container + }) + : oneOfExamples const handleExamplesSelect = (key /*, { isSyntheticChange } */) => { updateActiveExamplesKey(key) diff --git a/test/unit/core/plugins/oas3/request-body.js b/test/unit/core/plugins/oas3/request-body.js new file mode 100644 index 00000000000..ec63048fac6 --- /dev/null +++ b/test/unit/core/plugins/oas3/request-body.js @@ -0,0 +1,237 @@ +/** + * @prettier + */ +import { fromJS, OrderedMap } from "immutable" +import { getOneOfExamples } from "core/plugins/oas3/components/request-body" +import { + sampleFromSchema, + mergeJsonSchema, + memoizedSampleFromSchema, + memoizedCreateXMLExample, +} from "core/plugins/json-schema-5-samples/fn/index" +import makeGetSampleSchema from "core/plugins/json-schema-5-samples/fn/get-sample-schema" +import makeGetJsonSampleSchema from "core/plugins/json-schema-5-samples/fn/get-json-sample-schema" +import makeGetYamlSampleSchema from "core/plugins/json-schema-5-samples/fn/get-yaml-sample-schema" +import makeGetXmlSampleSchema from "core/plugins/json-schema-5-samples/fn/get-xml-sample-schema" + +const getSystem = () => ({ + fn: { + memoizedSampleFromSchema, + memoizedCreateXMLExample, + mergeJsonSchema, + sampleFromSchema, + getJsonSampleSchema: makeGetJsonSampleSchema(getSystem), + getYamlSampleSchema: makeGetYamlSampleSchema(getSystem), + getXmlSampleSchema: makeGetXmlSampleSchema(getSystem), + getSampleSchema: makeGetSampleSchema(getSystem), + }, +}) + +const buildRequestBody = (schema, extras = {}) => + fromJS({ + content: { + "application/json": { + schema, + ...extras, + }, + }, + }) + +describe("getOneOfExamples", () => { + const { fn } = getSystem() + const mediaType = "application/json" + + describe("when schema has oneOf with multiple variants", () => { + const schema = { + oneOf: [ + { + title: "RegisteredUser", + type: "object", + properties: { + userType: { type: "string", example: "registered" }, + username: { type: "string", example: "john_doe" }, + }, + }, + { + title: "GuestUser", + type: "object", + properties: { + userType: { type: "string", example: "guest" }, + displayName: { type: "string", example: "Guest-42" }, + }, + }, + ], + } + const requestBody = buildRequestBody(schema) + + it("returns an OrderedMap with one entry per variant", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + expect(OrderedMap.isOrderedMap(result)).toBe(true) + expect(result.size).toBe(2) + }) + + it("uses the variant title as the map key and summary", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + expect(result.has("RegisteredUser")).toBe(true) + expect(result.has("GuestUser")).toBe(true) + expect(result.getIn(["RegisteredUser", "summary"])).toBe("RegisteredUser") + expect(result.getIn(["GuestUser", "summary"])).toBe("GuestUser") + }) + + it("generates a distinct value for each variant", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + const registeredValue = result.getIn(["RegisteredUser", "value"]) + const guestValue = result.getIn(["GuestUser", "value"]) + expect(registeredValue).not.toEqual(guestValue) + }) + + it("each value contains the properties of its variant", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + const registered = JSON.parse(result.getIn(["RegisteredUser", "value"])) + const guest = JSON.parse(result.getIn(["GuestUser", "value"])) + expect(registered).toHaveProperty("username") + expect(guest).toHaveProperty("displayName") + }) + }) + + describe("when variant has no title but has a resolved $$ref", () => { + const schema = { + oneOf: [ + { + "$$ref": "#/components/schemas/RegisteredUser", + type: "object", + properties: { username: { type: "string" } }, + }, + { + "$$ref": "#/components/schemas/GuestUser", + type: "object", + properties: { displayName: { type: "string" } }, + }, + ], + } + const requestBody = buildRequestBody(schema) + + it("extracts the schema name from $$ref as the key", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + expect(result.has("RegisteredUser")).toBe(true) + expect(result.has("GuestUser")).toBe(true) + }) + }) + + describe("when variant has no title and no $$ref", () => { + const schema = { + oneOf: [ + { type: "object", properties: { a: { type: "string" } } }, + { type: "object", properties: { b: { type: "integer" } } }, + ], + } + const requestBody = buildRequestBody(schema) + + it("falls back to 'oneOf[index]' as the key", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + expect(result.has("oneOf[0]")).toBe(true) + expect(result.has("oneOf[1]")).toBe(true) + }) + }) + + describe("when variant has x-summary extension", () => { + const schema = { + anyOf: [ + { "x-summary": "Option A", type: "object", properties: { a: { type: "string" } } }, + { "x-summary": "Option B", type: "object", properties: { b: { type: "string" } } }, + ], + } + const requestBody = buildRequestBody(schema) + + it("uses x-summary as the key when no title is present", () => { + const result = getOneOfExamples(requestBody, mediaType, fn) + expect(result.has("Option A")).toBe(true) + expect(result.has("Option B")).toBe(true) + }) + }) + + describe("when schema has anyOf with multiple variants", () => { + it("falls back to 'anyOf[index]' keys when no titles are present", () => { + const schema = { + anyOf: [ + { type: "object", properties: { a: { type: "string" } } }, + { type: "object", properties: { b: { type: "string" } } }, + ], + } + const result = getOneOfExamples(buildRequestBody(schema), mediaType, fn) + expect(result.has("anyOf[0]")).toBe(true) + expect(result.has("anyOf[1]")).toBe(true) + }) + }) + + describe("when schema has only one variant", () => { + it("returns null (no dropdown needed for a single option)", () => { + const schema = { + oneOf: [ + { title: "OnlyOne", type: "object", properties: { a: { type: "string" } } }, + ], + } + const result = getOneOfExamples(buildRequestBody(schema), mediaType, fn) + expect(result).toBeNull() + }) + }) + + describe("when schema has no oneOf or anyOf", () => { + it("returns null", () => { + const schema = { + type: "object", + properties: { name: { type: "string" } }, + } + const result = getOneOfExamples(buildRequestBody(schema), mediaType, fn) + expect(result).toBeNull() + }) + }) + + describe("when explicit examples are already present on the media type", () => { + it("returns null and defers to the spec-defined examples", () => { + const schema = { + oneOf: [ + { title: "A", type: "object", properties: { a: { type: "string" } } }, + { title: "B", type: "object", properties: { b: { type: "string" } } }, + ], + } + const requestBody = buildRequestBody(schema, { + examples: { + myExample: { summary: "My example", value: { a: "hello" } }, + }, + }) + const result = getOneOfExamples(requestBody, mediaType, fn) + expect(result).toBeNull() + }) + }) + + describe("when parent schema has shared properties alongside oneOf", () => { + it("merges parent properties into each variant's generated sample", () => { + const schema = { + type: "object", + properties: { + id: { type: "integer", example: 42 }, + }, + oneOf: [ + { + title: "TypeA", + type: "object", + properties: { kindA: { type: "string", example: "a" } }, + }, + { + title: "TypeB", + type: "object", + properties: { kindB: { type: "string", example: "b" } }, + }, + ], + } + const result = getOneOfExamples(buildRequestBody(schema), mediaType, fn) + const typeA = JSON.parse(result.getIn(["TypeA", "value"])) + const typeB = JSON.parse(result.getIn(["TypeB", "value"])) + expect(typeA).toHaveProperty("id", 42) + expect(typeB).toHaveProperty("id", 42) + expect(typeA).toHaveProperty("kindA") + expect(typeB).toHaveProperty("kindB") + }) + }) +}) From bbfa3cdfb306d75455ac1a43661fd36fbfbe45ae Mon Sep 17 00:00:00 2001 From: adrianodpdiaz Date: Sat, 4 Apr 2026 21:54:23 +0200 Subject: [PATCH 2/2] fix/xml examples render notagname in the first render --- .../plugins/oas3/components/request-body.jsx | 17 ++++++++------ test/unit/core/plugins/oas3/request-body.js | 22 ++++++++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 1197ca21a9c..21c70020d2f 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -57,7 +57,8 @@ export const getOneOfExamples = (requestBody, mediaType, fn) => { // Build a parent schema that omits the oneOf/anyOf key so that // getSampleSchema generates a sample for each individual variant. - const { [keyword]: _omitted, ...parentWithoutVariants } = schemaJS + const parentWithoutVariants = { ...schemaJS } + delete parentWithoutVariants[keyword] const entries = variants.map((variant, index) => { const variantSchema = fn.mergeJsonSchema @@ -307,12 +308,14 @@ const RequestBody = ({ } - const sampleRequestBody = getDefaultRequestBodyValue( - requestBody, - contentType, - activeExamplesKey, - fn, - ) + const sampleRequestBody = oneOfExamples + ? (oneOfExamples.get(activeExamplesKey) ?? oneOfExamples.first())?.get("value") ?? "" + : getDefaultRequestBodyValue( + requestBody, + contentType, + activeExamplesKey, + fn, + ) let language = null let testValueForJson = getKnownSyntaxHighlighterLanguage(sampleRequestBody) if (testValueForJson) { diff --git a/test/unit/core/plugins/oas3/request-body.js b/test/unit/core/plugins/oas3/request-body.js index ec63048fac6..0b5e2ff1b41 100644 --- a/test/unit/core/plugins/oas3/request-body.js +++ b/test/unit/core/plugins/oas3/request-body.js @@ -98,12 +98,12 @@ describe("getOneOfExamples", () => { const schema = { oneOf: [ { - "$$ref": "#/components/schemas/RegisteredUser", + $$ref: "#/components/schemas/RegisteredUser", type: "object", properties: { username: { type: "string" } }, }, { - "$$ref": "#/components/schemas/GuestUser", + $$ref: "#/components/schemas/GuestUser", type: "object", properties: { displayName: { type: "string" } }, }, @@ -137,8 +137,16 @@ describe("getOneOfExamples", () => { describe("when variant has x-summary extension", () => { const schema = { anyOf: [ - { "x-summary": "Option A", type: "object", properties: { a: { type: "string" } } }, - { "x-summary": "Option B", type: "object", properties: { b: { type: "string" } } }, + { + "x-summary": "Option A", + type: "object", + properties: { a: { type: "string" } }, + }, + { + "x-summary": "Option B", + type: "object", + properties: { b: { type: "string" } }, + }, ], } const requestBody = buildRequestBody(schema) @@ -168,7 +176,11 @@ describe("getOneOfExamples", () => { it("returns null (no dropdown needed for a single option)", () => { const schema = { oneOf: [ - { title: "OnlyOne", type: "object", properties: { a: { type: "string" } } }, + { + title: "OnlyOne", + type: "object", + properties: { a: { type: "string" } }, + }, ], } const result = getOneOfExamples(buildRequestBody(schema), mediaType, fn)