Skip to content

fix: align OpenAPI 3.x.y file uploads with specification #10409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 11, 2025
7 changes: 7 additions & 0 deletions src/core/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ const defaultOptions = Object.freeze({
onComplete: null,
modelPropertyMacro: null,
parameterMacro: null,

fileUploadMediaTypes: [
"application/octet-stream",
"image/",
"audio/",
"video/",
],
})

export default defaultOptions
4 changes: 4 additions & 0 deletions src/core/config/type-cast/mappings.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ const mappings = {
docExpansion: { typeCaster: stringTypeCaster },
dom_id: { typeCaster: nullableStringTypeCaster },
domNode: { typeCaster: domNodeTypeCaster },
fileUploadMediaTypes: {
typeCaster: arrayTypeCaster,
defaultValue: defaultOptions.fileUploadMediaTypes,
},
filter: { typeCaster: filterTypeCaster },
fn: { typeCaster: objectTypeCaster },
initialState: { typeCaster: objectTypeCaster },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ export class JsonSchemaForm extends Component {
const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null
const foldedType = fn.jsonSchema202012.foldType(immutableToJS(type))
const isFileUploadIntended = fn.isFileUploadIntended(schema)

let getComponentSilently = (name) => getComponent(name, false, { failSilently: true })
let Comp = type ? format ?
getComponentSilently(`JsonSchema_${type}_${format}`) :
getComponentSilently(`JsonSchema_${type}`) :
getComponent("JsonSchema_string")

if (List.isList(type) && (foldedType === "array" || foldedType === "object")) {
if (!isFileUploadIntended && List.isList(type) && (foldedType === "array" || foldedType === "object")) {
Comp = getComponent("JsonSchema_object")
}

Expand Down
34 changes: 19 additions & 15 deletions src/core/plugins/oas3/components/request-body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,13 @@ const RequestBody = ({
}
requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List()

if(!mediaTypeValue.size) {
return null
}

const isObjectContent = mediaTypeValue.getIn(["schema", "type"]) === "object"
const isBinaryFormat = mediaTypeValue.getIn(["schema", "format"]) === "binary"
const isBase64Format = mediaTypeValue.getIn(["schema", "format"]) === "base64"
const isFileUploadIntended = fn.isFileUploadIntended(
mediaTypeValue?.get("schema"),
contentType
)

if(
contentType === "application/octet-stream"
|| contentType.indexOf("image/") === 0
|| contentType.indexOf("audio/") === 0
|| contentType.indexOf("video/") === 0
|| isBinaryFormat
|| isBase64Format
isFileUploadIntended
) {
const Input = getComponent("Input")

Expand All @@ -132,6 +124,18 @@ const RequestBody = ({
return <Input type={"file"} onChange={handleFile} />
}


if (!mediaTypeValue.size) {
return null
}

const schemaType = mediaTypeValue.getIn(["schema", "type"])
const isObjectContent =
schemaType === "object" ||
(specSelectors.isOAS31() &&
List.isList(schemaType) &&
schemaType.includes("object"))

if (
isObjectContent &&
(
Expand Down Expand Up @@ -190,11 +194,11 @@ const RequestBody = ({
initialValue = JSON.parse(initialValue)
}

const isFile = type === "string" && (format === "binary" || format === "base64")
const isFileUploadIntended = fn.isFileUploadIntended(schema)

const jsonSchemaForm = <JsonSchemaForm
fn={fn}
dispatchInitialValue={!isFile}
dispatchInitialValue={!isFileUploadIntended}
schema={schema}
description={key}
getComponent={getComponent}
Expand Down
25 changes: 25 additions & 0 deletions src/core/plugins/oas3/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @prettier
*/
import { objectify } from "core/utils"

export const makeIsFileUploadIntended = (getSystem) => {
const isFileUploadIntended = (schema, mediaType = null) => {
const { fileUploadMediaTypes } = getSystem().getConfigs()
const isFileUploadMediaType =
typeof mediaType === "string" &&
fileUploadMediaTypes.some((fileUploadMediaType) =>
mediaType.startsWith(fileUploadMediaType)
)

if (isFileUploadMediaType) {
return true
}

const { type, format } = objectify(schema)

return type === "string" && ["binary", "byte"].includes(format)
}

return isFileUploadIntended
}
9 changes: 8 additions & 1 deletion src/core/plugins/oas3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import wrapComponents from "./wrap-components"
import * as actions from "./actions"
import * as selectors from "./selectors"
import reducers from "./reducers"
import { makeIsFileUploadIntended } from "./fn"

export default function ({ getSystem }) {
const isFileUploadIntended = makeIsFileUploadIntended(getSystem)

export default function () {
return {
components,
wrapComponents,
Expand All @@ -28,5 +31,9 @@ export default function () {
selectors: { ...selectors },
},
},
fn: {
isFileUploadIntended,
isFileUploadIntendedOAS30: isFileUploadIntended,
},
}
}
8 changes: 4 additions & 4 deletions src/core/plugins/oas3/wrap-components/json-schema-string.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
schema,
getComponent,
errors,
onChange
onChange,
fn
} = props

const format = schema && schema.get ? schema.get("format") : null
const type = schema && schema.get ? schema.get("type") : null
const isFileUploadIntended = fn.isFileUploadIntended(schema)
const Input = getComponent("Input")

if(type && type === "string" && (format && (format === "binary" || format === "base64"))) {
if (isFileUploadIntended) {
return <Input type="file"
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
Expand Down
11 changes: 11 additions & 0 deletions src/core/plugins/oas31/after-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getProperties,
} from "./json-schema-2020-12-extensions/fn"
import { wrapOAS31Fn } from "./fn"
import { makeIsFileUploadIntended } from "./oas3-extensions/fn"

function afterLoad({ fn, getSystem }) {
// overrides for fn.jsonSchema202012
Expand Down Expand Up @@ -38,6 +39,16 @@ function afterLoad({ fn, getSystem }) {

Object.assign(this.fn, wrappedFns)
}

// overrides behavior in OpenAPI 3.1.x, recognizes more intentions
const isFileUploadIntended = makeIsFileUploadIntended(getSystem)
const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS31Fn(
{ isFileUploadIntended },
getSystem()
)

this.fn.isFileUploadIntended = isFileUploadIntendedWrap
this.fn.isFileUploadIntendedOAS31 = isFileUploadIntended
}

export default afterLoad
27 changes: 27 additions & 0 deletions src/core/plugins/oas31/oas3-extensions/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @prettier
*/
import { objectify } from "core/utils"

export const makeIsFileUploadIntended = (getSystem) => {
const isFileUploadIntended = (schema, mediaType = null) => {
const { fileUploadMediaTypes } = getSystem().getConfigs()
const isFileUploadMediaType =
typeof mediaType === "string" &&
fileUploadMediaTypes.some((fileUploadMediaType) =>
mediaType.startsWith(fileUploadMediaType)
)

if (isFileUploadMediaType) {
return true
}

const { type, format } = objectify(schema)
const includesStringType =
type === "string" || (Array.isArray(type) && type.includes("string"))

return includesStringType && ["binary", "byte"].includes(format)
}

return isFileUploadIntended
}
Loading