diff --git a/src/core/containers/OperationContainer.jsx b/src/core/containers/OperationContainer.jsx index 608ba391a93..e0fcc2a24ab 100644 --- a/src/core/containers/OperationContainer.jsx +++ b/src/core/containers/OperationContainer.jsx @@ -127,6 +127,29 @@ export default class OperationContainer extends PureComponent { if (contentType === "application/x-www-form-urlencoded" || contentType === "multipart/form-data") { const jsonRequestBodyValue = JSON.parse(defaultRequestBodyValue) + + try { + const [pathName, method] = pathMethod + const requestBodyDef = this.props.specSelectors.specResolvedSubtree([ + "paths", + pathName, + method, + "requestBody", + ]) + const schemaForMediaType = requestBodyDef?.getIn(["content", contentType, "schema"]) || Map() + const properties = schemaForMediaType.get("properties") || Map() + if (properties && typeof jsonRequestBodyValue === "object" && jsonRequestBodyValue !== null) { + properties.keySeq().toArray().forEach((key) => { + const propSchema = properties.get(key) || Map() + const schemaType = propSchema.get("type") + if (schemaType === "string" && key in jsonRequestBodyValue && typeof jsonRequestBodyValue[key] !== "string") { + jsonRequestBodyValue[key] = String(jsonRequestBodyValue[key]) + } + }) + } + } catch (e) { + // noop: fallback to parsed defaults without coercion + } Object.entries(jsonRequestBodyValue).forEach(([key, value]) => { if (Array.isArray(value)) { jsonRequestBodyValue[key] = jsonRequestBodyValue[key].map((val) => { diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx index 29e7342882a..4e85b565f6f 100644 --- a/src/core/plugins/oas3/components/request-body.jsx +++ b/src/core/plugins/oas3/components/request-body.jsx @@ -19,7 +19,7 @@ export const getDefaultRequestBodyValue = (requestBody, mediaType, activeExample ]) : exampleSchema - const exampleValue = fn.getSampleSchema( + let exampleValue = fn.getSampleSchema( schema, mediaType, { @@ -27,6 +27,16 @@ export const getDefaultRequestBodyValue = (requestBody, mediaType, activeExample }, mediaTypeExample ) + + const isFormLike = mediaType === "application/x-www-form-urlencoded" || mediaType.indexOf("multipart/") === 0 + if (isFormLike && schema && schema.type === "object" && schema.properties && exampleValue && typeof exampleValue === "object") { + Object.entries(schema.properties).forEach(([prop, propSchema]) => { + const propType = Array.isArray(propSchema?.type) ? propSchema.type[0] : propSchema?.type + if (propType === "string" && propSchema && Object.prototype.hasOwnProperty.call(propSchema, "default")) { + exampleValue[prop] = String(propSchema.default) + } + }) + } return stringify(exampleValue) } @@ -179,6 +189,18 @@ const RequestBody = ({ initialValue = "0" } + // If schema defines a default and the property is string-typed, prefer it verbatim. + // This preserves values like "1.0" exactly as specified by the spec. + if (objectType === "string") { + const schemaDefault = schema.get("default") + if (schemaDefault !== undefined) { + initialValue = String(schemaDefault) + } else if (typeof initialValue !== "string") { + // Otherwise ensure the sampled value is coerced to string. + initialValue = String(initialValue) + } + } + if (typeof initialValue !== "string" && objectType === "object") { initialValue = stringify(initialValue) } diff --git a/src/core/plugins/spec/actions.js b/src/core/plugins/spec/actions.js index 8ea0c5801b2..4fbdcf411bc 100644 --- a/src/core/plugins/spec/actions.js +++ b/src/core/plugins/spec/actions.js @@ -428,13 +428,27 @@ export const executeRequest = (req) => const requestBodyInclusionSetting = oas3Selectors.requestBodyInclusionSetting(pathName, method) if(requestBody && requestBody.toJS) { + const contentType = req.requestContentType + const requestBodyDef = specSelectors.specResolvedSubtree([ + "paths", + pathName, + method, + "requestBody", + ]) + const schemaForMediaType = requestBodyDef?.getIn(["content", contentType, "schema"]) || ImmutableMap() + const properties = schemaForMediaType.get("properties") || ImmutableMap() + req.requestBody = requestBody .map( - (val) => { - if (ImmutableMap.isMap(val)) { - return val.get("value") + (val, key) => { + const propSchema = properties.get(key) || ImmutableMap() + const schemaType = propSchema.get("type") + let outVal = ImmutableMap.isMap(val) ? val.get("value") : val + // If schema says string but value is not string, coerce + if (schemaType === "string" && typeof outVal !== "string" && outVal !== undefined && outVal !== null) { + outVal = String(outVal) } - return val + return outVal } ) .filter( diff --git a/test/e2e-cypress/e2e/bugs/10047.cy.js b/test/e2e-cypress/e2e/bugs/10047.cy.js new file mode 100644 index 00000000000..8f96e3ce0e3 --- /dev/null +++ b/test/e2e-cypress/e2e/bugs/10047.cy.js @@ -0,0 +1,62 @@ +describe("#10047: String default '1.0' preserved for form-urlencoded", () => { + it("should preserve string default value '1.0' exactly as specified", () => { + cy + .visit("?url=/documents/bugs/10047.yaml") + .get("#operations-default-testOp") + .click() + // Expand Try It Out + .get(".try-out__btn") + .click() + // Check that the default value is "1.0" (not "1") + .get(`.parameters[data-property-name="versionTag"] input`) + .should("have.value", "1.0") + // Execute the request and check the curl command + .get(".btn.execute") + .click() + // Check that the curl command contains versionTag=1.0 + .get(".curl") + .should("contain", "versionTag=1.0") + .should("not.contain", "versionTag=1") + }) + + it("should preserve user-entered value '2.0' correctly", () => { + cy + .visit("?url=/documents/bugs/10047.yaml") + .get("#operations-default-testOp") + .click() + // Expand Try It Out + .get(".try-out__btn") + .click() + // Change the value + .get(`.parameters[data-property-name="versionTag"] input`) + .clear() + .type("2.0") + // Execute the request + .get(".btn.execute") + .click() + // Check that the curl command contains versionTag=2.0 + .get(".curl") + .should("contain", "versionTag=2.0") + }) + + it("should reset to default value '1.0' correctly", () => { + cy + .visit("?url=/documents/bugs/10047.yaml") + .get("#operations-default-testOp") + .click() + // Expand Try It Out + .get(".try-out__btn") + .click() + // Change the value + .get(`.parameters[data-property-name="versionTag"] input`) + .clear() + .type("2.0") + // Reset + .get(".try-out__btn.reset") + .click() + // Check that the value is reset to "1.0" (not "1") + .get(`.parameters[data-property-name="versionTag"] input`) + .should("have.value", "1.0") + }) +}) + diff --git a/test/e2e-cypress/static/documents/bugs/10047.yaml b/test/e2e-cypress/static/documents/bugs/10047.yaml new file mode 100644 index 00000000000..4ce22a2cb39 --- /dev/null +++ b/test/e2e-cypress/static/documents/bugs/10047.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.1 +info: + title: Reproducer + version: "1.0" +paths: + /test: + post: + operationId: testOp + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Example' + responses: + 200: + description: "Success" + +components: + schemas: + Example: + type: object + properties: + versionTag: + type: string + default: "1.0" +