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
21 changes: 21 additions & 0 deletions src/core/plugins/json-schema-2020-12/fn.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* @prettier
*/
import { List, Map } from "immutable"

export const upperFirst = (value) => {
if (typeof value === "string") {
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`
Expand Down Expand Up @@ -504,3 +506,22 @@ export const makeGetExtensionKeywords = (fnAccessor) => {

return getExtensionKeywords
}

export const hasSchemaType = (schema, type) => {
const isSchemaImmutable = Map.isMap(schema)

if (!isSchemaImmutable && !isPlainObject(schema)) {
return false
}

const hasType = (schemaType) =>
type === schemaType || (Array.isArray(type) && type.includes(schemaType))

const schemaType = isSchemaImmutable ? schema.get("type") : schema.type

if (List.isList(schemaType) || Array.isArray(schemaType)) {
return schemaType.some((t) => hasType(t))
}

return hasType(schemaType)
}
2 changes: 2 additions & 0 deletions src/core/plugins/json-schema-2020-12/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
isBooleanJSONSchema,
getSchemaKeywords,
makeGetExtensionKeywords,
hasSchemaType,
} from "./fn"
import { JSONSchemaPathContext, JSONSchemaLevelContext } from "./context"
import {
Expand Down Expand Up @@ -143,6 +144,7 @@ const JSONSchema202012Plugin = ({ getSystem, fn }) => {
useLevel,
getSchemaKeywords,
getExtensionKeywords: makeGetExtensionKeywords(fnAccessor),
hasSchemaType,
},
},
}
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
19 changes: 19 additions & 0 deletions src/core/plugins/json-schema-5/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @prettier
*/
import { Map } from "immutable"
import isPlainObject from "lodash/isPlainObject"

export const hasSchemaType = (schema, type) => {
const isSchemaImmutable = Map.isMap(schema)

if (!isSchemaImmutable && !isPlainObject(schema)) {
return false
}

const schemaType = isSchemaImmutable ? schema.get("type") : schema.type

return (
type === schemaType || (Array.isArray(type) && type.includes(schemaType))
)
}
4 changes: 4 additions & 0 deletions src/core/plugins/json-schema-5/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Schemes from "./components/schemes"
import SchemesContainer from "./containers/schemes"
import * as JSONSchemaComponents from "./components/json-schema-components"
import { ModelExtensions } from "./components/model-extensions"
import { hasSchemaType } from "./fn"

const JSONSchema5Plugin = () => ({
components: {
Expand All @@ -31,6 +32,9 @@ const JSONSchema5Plugin = () => ({
SchemesContainer,
...JSONSchemaComponents,
},
fn: {
hasSchemaType,
},
})

export default JSONSchema5Plugin
29 changes: 14 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,13 @@ const RequestBody = ({
return <Input type={"file"} onChange={handleFile} />
}


if (!mediaTypeValue.size) {
return null
}

const isObjectContent = fn.hasSchemaType(mediaTypeValue.get("schema"), "object")

if (
isObjectContent &&
(
Expand Down Expand Up @@ -190,11 +189,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
35 changes: 35 additions & 0 deletions src/core/plugins/oas3/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @prettier
*/
import { Map } from "immutable"
import isPlainObject from "lodash/isPlainObject"

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

if (isFileUploadMediaType) {
return true
}

const isSchemaImmutable = Map.isMap(schema)

if (!isSchemaImmutable && !isPlainObject(schema)) {
return false
}

const format = isSchemaImmutable ? schema.get("format") : schema.format

return (
fn.hasSchemaType(schema, "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
24 changes: 24 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,29 @@ 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

if (fn.jsonSchema202012) {
const { hasSchemaType } = wrapOAS31Fn(
{
hasSchemaType: fn.jsonSchema202012.hasSchemaType,
},
getSystem()
)

this.fn.hasSchemaType = hasSchemaType
}
}

export default afterLoad
13 changes: 13 additions & 0 deletions src/core/plugins/oas31/oas3-extensions/fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @prettier
*/

export const makeIsFileUploadIntended = (getSystem) => {
const isFileUploadIntended = (schema, mediaType = null) => {
const { fn } = getSystem()

return fn.isFileUploadIntendedOAS30(schema, mediaType)
}

return isFileUploadIntended
}
Loading