diff --git a/.claude/skills/add-oas-support.md b/.claude/skills/add-oas-support.md
index 0709b242d90..36062c30ab4 100644
--- a/.claude/skills/add-oas-support.md
+++ b/.claude/skills/add-oas-support.md
@@ -152,6 +152,13 @@ See [full mapping table with examples](#step-1d-map-specification-changes-to-swa
5. ❌ **Single quotes** → ✅ Use double quotes
6. ❌ **Skipping spec examples** → ✅ Use as test fixtures
7. ❌ **Hardcoded assumptions** → ✅ Verify everything in spec
+8. ❌ **`props.Ori` in ComponentWrapper** → ✅ Use `props.originalComponent` (see Pattern 3)
+9. ❌ **Copying entire Info component for minor version** → ✅ Use OpenAPIVersion wrapper + `getComponent("OAS{PREV}Info")`
+10. ❌ **OAS{VERSION} logic in OAS{PREV} plugin** → ✅ Each version's logic lives in its own plugin
+11. ❌ **`isOAS{PREV}` wrapper on minor version** → ✅ Only add if regex overlaps; minor bumps don't need it
+12. ❌ **New HTTP method in core `validOperationMethods`** → ✅ Add to `OPERATION_METHODS` + wrap with `createOnlyOAS{VERSION}SelectorWrapper`
+13. ❌ **Assuming new OAS meta-schema = new JSON Schema dialect** → ✅ Verify — OAS 3.2 still uses JSON Schema 2020-12
+14. ❌ **Inline version guard in wrap-components** → ✅ Always use `createOnlyOAS{VERSION}ComponentWrapper`; never write `(Original, system) => (props) => { if (...isOAS{VERSION}...) }` by hand — even expanding an existing guard (`isOAS31 || isOAS32`) is wrong; add dedicated wrap-components in the new plugin instead
### ✅ Pre-Submit Checklist
@@ -268,11 +275,26 @@ export const createOnlyOAS{VERSION}SelectorWrapper =
```
### Pattern 3: Component Wrappers
+
+**⚠️ IMPORTANT: `originalComponent` prop name, not `Ori`**
+
+`createOnlyOAS{VERSION}ComponentWrapper` passes the original component as `originalComponent` in props (NOT `Ori` — that's the OAS3/OAS30ComponentWrapFactory convention). Use `const { originalComponent: Ori } = props` to access it.
+
+**Pattern A — Reuse previous version's component via getComponent:**
```javascript
const ComponentWrapper = createOnlyOAS{VERSION}ComponentWrapper(({ getSystem }) => {
const system = getSystem()
- const OAS{VERSION}Component = system.getComponent("OAS{VERSION}Component", true)
- return
+ const OAS{PREV_VERSION}Component = system.getComponent("OAS{PREV_VERSION}Component", true)
+ return
+})
+```
+
+**Pattern B — Render original component with extra props (e.g. version badge):**
+```javascript
+// Use `originalComponent` (NOT `Ori`) — that's the prop name createOnlyOAS{VERSION}ComponentWrapper passes
+const OpenAPIVersionWrapper = createOnlyOAS{VERSION}ComponentWrapper((props) => {
+ const { originalComponent: Ori } = props
+ return
})
```
@@ -839,15 +861,25 @@ export const createSystemSelector =
}
/**
- * Creates a component wrapper that only renders for OAS {VERSION}
+ * Creates a component wrapper that only renders for OAS {VERSION}.
+ * When active, passes `originalComponent` (the unwrapped original) and
+ * `getSystem` as extra props. Access the original via:
+ * const { originalComponent: Ori } = props
+ * NOT via `props.Ori` — that's the OAS3/OAS30ComponentWrapFactory convention.
*/
export const createOnlyOAS{VERSION}ComponentWrapper =
- (Component) =>
- ({ ...props }) => {
- const { specSelectors } = props
- const isOAS{VERSION} = specSelectors.isOAS{VERSION}()
+ (Component) => (Original, system) => (props) => {
+ if (system.specSelectors.isOAS{VERSION}()) {
+ return (
+
+ )
+ }
- return isOAS{VERSION} ? : props.Ori()
+ return
}
/**
@@ -1008,6 +1040,8 @@ export const selectIsOAS{VERSION} = (state, system) => () => {
**File:** `src/core/plugins/oas{VERSION_NUMBER}/spec-extensions/wrap-selectors.js`
+**⚠️ Minor version: DON'T wrap `isOAS{PREV}` unless the regex actually matches both versions.** For example, OAS 3.1's `isOAS31` regex (`/^3\.1\./`) will never match `3.2.x`, so wrapping it to return `false` is dead code. Only add the `isOAS{PREV}` override if the previous version's detection regex would incorrectly match the new version.
+
**Template:**
```javascript
/**
@@ -1016,11 +1050,17 @@ export const selectIsOAS{VERSION} = (state, system) => () => {
import { createOnlyOAS{VERSION}SelectorWrapper } from "../fn.js"
-// Wrap previous version selectors if behavior changes
-// Example:
-// export const isOAS{PREVIOUS_VERSION} = createOnlyOAS{VERSION}SelectorWrapper((state) => () => false)
+// Ensure OAS {VERSION} specs are recognized as OAS 3.x (needed when major version number didn't change)
+export const isOAS3 =
+ (oriSelector, system) =>
+ (state, ...args) => {
+ const isOAS{VERSION} = system.specSelectors.isOAS{VERSION}()
+ return isOAS{VERSION} || oriSelector(...args)
+ }
-// This makes isOAS{PREVIOUS_VERSION} return false when spec is OAS {VERSION}
+// ONLY add isOAS{PREV} wrapper if the previous version's regex could match this new version.
+// For a minor version bump (e.g. 3.1 → 3.2), the previous regex won't match, so DON'T add this.
+// export const isOAS{PREV} = createOnlyOAS{VERSION}SelectorWrapper((state) => () => false)
```
### Step 6: Create Version Pragma Filter Component
@@ -1135,23 +1175,51 @@ export default NewFeature
**File:** `src/core/plugins/oas{VERSION_NUMBER}/wrap-components/info.jsx`
-**Template:**
+**⚠️ Minor version (e.g. 3.2): don't create a new Info component.** If the Info object is identical to the previous version, reuse `OAS{PREV_VERSION}Info` via `getComponent` instead of copying the whole component. Only create a new Info component for major versions with significant structural changes.
+
+**Template (minor version — reuse previous Info):**
```javascript
/**
* @prettier
*/
+import React from "react"
import { createOnlyOAS{VERSION}ComponentWrapper } from "../fn.js"
const InfoWrapper = createOnlyOAS{VERSION}ComponentWrapper(({ getSystem }) => {
const system = getSystem()
- const OAS{VERSION}Info = system.getComponent("OAS{VERSION}Info", true)
- return
+ const OAS{PREV_VERSION}Info = system.getComponent("OAS{PREV_VERSION}Info", true)
+ return
})
export default InfoWrapper
```
+**Also — OpenAPIVersion wrapper (minor version — change version badge only):**
+
+For minor versions where the only Info difference is the version badge, use the OpenAPIVersion wrapper pattern instead of wrapping InfoContainer at all:
+
+```javascript
+// wrap-components/openapi-version.jsx
+/**
+ * @prettier
+ */
+import React from "react"
+import { createOnlyOAS{VERSION}ComponentWrapper } from "../fn.js"
+
+export default createOnlyOAS{VERSION}ComponentWrapper((props) => {
+ const { originalComponent: Ori } = props // NOT `Ori` from props directly — use `originalComponent`
+ return
+})
+```
+
+Then register it in index.js:
+```javascript
+wrapComponents: {
+ OpenAPIVersion: OpenAPIVersionWrapper, // changes the version badge shown in the Info header
+}
+```
+
### Step 9: Implement afterLoad Hook
**File:** `src/core/plugins/oas{VERSION_NUMBER}/after-load.js`
@@ -1531,11 +1599,23 @@ Closes #{ISSUE_NUMBER}
6. **Test with real specs** - Use actual OAS {VERSION} examples
7. **Don't modify core unnecessarily** - Use plugin architecture
8. **Version regex must be exact** - Follow pattern from OAS 3.1
-9. **Component wrappers return Ori()** - When not active version
+9. **Component wrappers render ``** - When not active version (handled by `createOnlyOAS{VERSION}ComponentWrapper` automatically)
10. **afterLoad runs after all plugins** - Safe to modify system
+11. **`originalComponent` not `Ori`** - `createOnlyOAS{VERSION}ComponentWrapper` passes the original as `originalComponent` prop. Use `const { originalComponent: Ori } = props`. The `Ori` name comes from `OAS30ComponentWrapFactory` which uses a different signature.
+12. **Don't copy the entire Info component for minor versions** - Use the OpenAPIVersion wrapper to change the version badge, and reuse the previous version's Info via `getComponent("OAS{PREV}Info", true)` instead of copying.
+13. **Don't add version-specific logic to previous version's plugin** - OAS32 logic belongs in the OAS32 plugin, not in OAS31 plugin files (e.g. don't add `isOAS32` checks to `oas31/wrap-components/license.jsx`).
+20. **Always use `createOnlyOAS{VERSION}ComponentWrapper` in wrap-components — never inline the version guard** - Each wrap-component in a plugin should use the factory (`createOnlyOAS{VERSION}ComponentWrapper`), not a hand-written `(Original, system) => (props) => { if (system.specSelectors.isOAS{VERSION}()) ... }`. When a later version (OAS32) also needs to reuse the same OAS31 component, it gets its own dedicated wrap-component in the OAS32 plugin — don't expand the guard to `isOAS31 || isOAS32` in the OAS31 file. The OAS31 wrappers for contact/license should use `createOnlyOAS31ComponentWrapper` and nothing else; OAS32 contact/license wrappers live in `oas32/wrap-components/` and handle the OAS32 case.
+14. **Don't add `isOAS{PREV}` wrap-selector unless the regex actually overlaps** - For minor versions, the previous version's regex already won't match (e.g. OAS31's `/^3\.1\./` won't match `3.2.x`). Adding the wrapper is dead code.
+15. **New HTTP methods: use `OPERATION_METHODS` in `spec/selectors.js`, not `operationsWithRootInherited` wrapper** - Adding the method to `OPERATION_METHODS` makes the core `operations` selector collect those ops. The `validOperationMethods` wrapper (guarded by `createOnlyOAS{VERSION}SelectorWrapper`) then controls whether the UI renders them. No need for an `operationsWithRootInherited` wrapper.
+16. **Don't add new HTTP methods to the core `validOperationMethods` constant** - Adding `"query"` to the base constant in `spec/selectors.js` affects ALL OAS versions. Instead, add it only via `createOnlyOAS{VERSION}SelectorWrapper` in your plugin's `spec-extensions/wrap-selectors.js`.
+17. **Don't create selectors only used in tests** - Selectors like `selectHasQueryOperations` that are never called from production code should not exist. Remove them.
+18. **OAS meta-schema URL ≠ new JSON Schema dialect** - The URL `https://spec.openapis.org/oas/3.2/schema/...` is the OAS 3.2 document structure schema, NOT a new JSON Schema version. OAS 3.2 uses JSON Schema 2020-12, the same as OAS 3.1. Don't list "new JSON Schema version" as a feature unless it actually changes.
+19. **Verify "not yet implemented" feature list against previous version** - Features like `pathItems in Components` were already in OAS 3.1, not new in 3.2. Check what's actually new before listing it.
## JSON Schema Version Changes
+**⚠️ First, verify whether the JSON Schema version actually changed.** The OAS meta-schema URL (e.g. `https://spec.openapis.org/oas/3.2/schema/...`) describes the OAS document structure — it is NOT a new JSON Schema dialect. OAS 3.2 uses JSON Schema 2020-12, the same as OAS 3.1. Only create a new `json-schema-{VERSION}` plugin if the actual JSON Schema dialect changed (as it did from OAS 3.0 Draft-07 → OAS 3.1 JSON Schema 2020-12).
+
If the new OAS version uses a different JSON Schema version:
1. **Create json-schema-{VERSION} plugin** (separate from OAS plugin)
@@ -1574,16 +1654,24 @@ Example from OAS 3.1 using JSON Schema 2020-12:
- Backward-compatible additions
- New optional fields
- Enhanced existing features
-- Same JSON Schema version
+- Same JSON Schema version (verify before assuming it changed)
- Incremental improvements
**Implementation approach:**
- Lighter plugin with focused additions
- Fewer component wrappers needed
- Selective selector additions
-- Reuse most of previous version logic
+- Reuse most of previous version logic via `getComponent("OAS{PREV}...", true)`
- Simpler afterLoad modifications
+**Key decisions for minor versions (lessons from OAS 3.2):**
+
+1. **Version badge only changed?** Use `OpenAPIVersion` wrapper + reuse `OAS{PREV}Info` — don't create a new Info component.
+2. **New HTTP method (e.g. QUERY)?** Add it to `OPERATION_METHODS` in `src/core/plugins/spec/selectors.js` AND add it to `validOperationMethods` via `createOnlyOAS{VERSION}SelectorWrapper`. Don't add it to the core `validOperationMethods` constant (affects all versions).
+3. **`isOAS{PREV}` wrapper needed?** Only if the previous regex also matches the new version string. Minor version bumps (3.1 → 3.2) don't need it.
+4. **JSON Schema version comment?** Verify it actually changed. OAS 3.2 uses JSON Schema 2020-12, the same as OAS 3.1. The new OAS meta-schema URL (`https://spec.openapis.org/oas/3.2/schema/...`) describes OAS document structure, not a new JSON Schema dialect.
+5. **"Not yet implemented" list?** Double-check each item against the *previous* version's changelog — some may already be implemented.
+
## Example: Adding OAS 4.0 (Major)
**Version detection:**
@@ -1632,16 +1720,56 @@ export const isOAS32 = (jsSpec) => {
**Directory:** `src/core/plugins/oas32/`
-**Extend OAS 3.1:**
+**Lighter component wrapping — OAS 3.2 specific patterns:**
+
+```javascript
+// wrap-components/openapi-version.jsx — change the version badge only
+// Use `originalComponent` (NOT `Ori`) — that's what createOnlyOAS32ComponentWrapper passes
+export default createOnlyOAS32ComponentWrapper((props) => {
+ const { originalComponent: Ori } = props
+ return
+})
+
+// wrap-components/info.jsx — reuse OAS31Info, don't create a new one
+export default createOnlyOAS32ComponentWrapper(({ getSystem }) => {
+ const system = getSystem()
+ const OAS31Info = system.getComponent("OAS31Info", true)
+ return
+})
+
+// wrap-components/contact.jsx and license.jsx — same pattern (OAS32 logic belongs HERE, not in OAS31)
+export default createOnlyOAS32ComponentWrapper((props) => {
+ const { getSystem } = props
+ const system = getSystem()
+ const OAS31Contact = system.getComponent("OAS31Contact", true)
+ return
+})
+```
+
+**New HTTP method (QUERY) — two-part approach:**
+
+Part 1: Add to `OPERATION_METHODS` in `src/core/plugins/spec/selectors.js` so the `operations` selector collects QUERY ops:
+```javascript
+// spec/selectors.js
+export const OPERATION_METHODS = [
+ "get", "put", "post", "delete", "options", "head", "patch", "trace", "query",
+]
+```
+
+Part 2: Add to `validOperationMethods` via `createOnlyOAS32SelectorWrapper` so the UI only renders them for OAS 3.2:
```javascript
-// oas31-extensions/fn.js
-// Import functions from oas31 and extend as needed
+// oas32/spec-extensions/wrap-selectors.js
+export const validOperationMethods = createOnlyOAS32SelectorWrapper(
+ () => (oriSelector, system) => system.oas32Selectors.validOperationMethods()
+)
+
+// oas32/selectors.js
+export const validOperationMethods = () => [
+ "get", "put", "post", "delete", "options", "head", "patch", "trace", "query",
+]
```
-**Lighter component wrapping:**
-- Only wrap components that change
-- Reuse most OAS 3.1 logic
-- Minimal selector additions
+Do NOT add `"query"` to the core `validOperationMethods` constant — that would affect all versions.
## Final Checklist
diff --git a/CLAUDE.md b/CLAUDE.md
index f58b0b4673b..e7bc2e818e5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,7 +1,7 @@
# CLAUDE.md - Swagger UI Codebase Guide
-> **Last Updated:** 2026-01-21
-> **Version:** 5.31.0
+> **Last Updated:** 2026-02-24
+> **Version:** 5.32.0 (in development)
> **Purpose:** Comprehensive guide for AI assistants working with the Swagger UI codebase
---
@@ -124,6 +124,7 @@ Swagger UI uses a **sophisticated plugin system** powered by Redux. The core sys
- `logs` - Logging
- `oas3` - OpenAPI 3.0.x support
- `oas31` - OpenAPI 3.1.x support
+- `oas32` - OpenAPI 3.2.x support
- `on-complete` - Completion callbacks
- `request-snippets` - Code snippet generation
- `safe-render` - Safe component rendering
@@ -571,6 +572,42 @@ Each plugin has:
See documentation: `docs/customization/plugin-api.md`
+### Cross-Plugin Import Guidelines
+
+**IMPORTANT:** Avoid cross-plugin imports to maintain plugin independence and modularity.
+
+**Pattern to Follow:**
+- Each plugin should be self-contained with its own components, utilities, and functions
+- When OAS version plugins (oas3, oas31, oas32) need similar functionality, create self-contained copies within each plugin
+- Wrap components should import from their own plugin's components, not from other plugins
+
+**Example Structure:**
+```
+src/core/plugins/oas32/
+├── json-schema-2020-12-extensions/
+│ ├── components/ # Self-contained components
+│ │ └── keywords/
+│ │ ├── Description.jsx
+│ │ └── Properties.jsx
+│ ├── wrap-components/ # Wrappers for components
+│ │ └── keywords/
+│ │ ├── Description.jsx # Imports from ../../components/
+│ │ └── Properties.jsx # Not from ../../../../oas31/
+│ └── fn.js # Self-contained utilities
+```
+
+**Why This Matters:**
+- Prevents tight coupling between plugins
+- Makes plugins easier to test in isolation
+- Allows independent versioning and updates
+- Reduces risk of breaking changes across plugins
+- Improves code maintainability
+
+**Exceptions:**
+- Shared core utilities in `src/core/utils/` are acceptable
+- System-level functions in `src/core/system.js` are acceptable
+- Base components in `src/core/components/` are acceptable
+
### Preset System
**Base Preset:** `src/core/presets/base.js`
@@ -813,6 +850,7 @@ dist/ # Build output (generated)
- OAS 2.0: Use `src/core/plugins/swagger-client/`
- OAS 3.0.x: Use `src/core/plugins/oas3/`
- OAS 3.1.x: Use `src/core/plugins/oas31/`
+- OAS 3.2.x: Use `src/core/plugins/oas32/`
**Adding Test Specs:**
- Add to `test/e2e-cypress/static/documents/`
@@ -839,6 +877,7 @@ dist/ # Build output (generated)
13. **Use the plugin architecture** - don't modify core unnecessarily
14. **Preserve backward compatibility** unless explicitly breaking
15. **Run full test suite before submitting PR**
+16. **Keep plugins self-contained** - avoid cross-plugin imports (see [Cross-Plugin Import Guidelines](#cross-plugin-import-guidelines))
### DON'Ts ❌
@@ -857,6 +896,7 @@ dist/ # Build output (generated)
13. **Don't ignore Cypress test failures**
14. **Don't add dependencies without justification**
15. **Don't break the build** - verify with `npm run build`
+16. **Don't import from other plugins** - create self-contained copies instead (e.g., don't import from `oas31` in `oas32`)
### When Working with AI Assistants
diff --git a/README.md b/README.md
index c7f74defcfa..d538a7fc4d7 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ The OpenAPI Specification has undergone 5 revisions since initial creation in 20
| Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes |
|--------------------|--------------|-------------------------------------------------------------|-----------------------------------------------------------------------|
+| 5.32.0 | TBD | 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.1.0, 3.1.1, 3.1.2, 3.2.0 | [next release](https://github.com/swagger-api/swagger-ui/tree/master) |
| 5.19.0 | 2025-02-17 | 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.1.0, 3.1.1, 3.1.2 | [tag v5.19.0](https://github.com/swagger-api/swagger-ui/tree/v5.19.0) |
| 5.0.0 | 2023-06-12 | 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0 | [tag v5.0.0](https://github.com/swagger-api/swagger-ui/tree/v5.0.0) |
| 4.0.0 | 2021-11-03 | 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3 | [tag v4.0.0](https://github.com/swagger-api/swagger-ui/tree/v4.0.0) |
diff --git a/src/core/components/layouts/base.jsx b/src/core/components/layouts/base.jsx
index efd61551c36..04807aa2ee7 100644
--- a/src/core/components/layouts/base.jsx
+++ b/src/core/components/layouts/base.jsx
@@ -34,6 +34,7 @@ export default class BaseLayout extends React.Component {
const isSwagger2 = specSelectors.isSwagger2()
const isOAS3 = specSelectors.isOAS3()
const isOAS31 = specSelectors.isOAS31()
+ const isOAS32 = specSelectors.isOAS32()
const isSpecEmpty = !specSelectors.specStr()
@@ -100,6 +101,8 @@ export default class BaseLayout extends React.Component {
}
>
diff --git a/src/core/plugins/oas31/components/version-pragma-filter.jsx b/src/core/plugins/oas31/components/version-pragma-filter.jsx
index 58b7616aa41..eaf3594c2b9 100644
--- a/src/core/plugins/oas31/components/version-pragma-filter.jsx
+++ b/src/core/plugins/oas31/components/version-pragma-filter.jsx
@@ -26,9 +26,9 @@ const VersionPragmaFilter = ({
one of the fields.
- Supported version fields are swagger: "2.0" and
- those that match openapi: 3.x.y (for example,{" "}
- openapi: 3.1.0).
+ Supported version fields are swagger: "2.0",{" "}
+ openapi: 3.0.x, or openapi: 3.1.x (for
+ example, openapi: 3.1.0).
@@ -48,9 +48,9 @@ const VersionPragmaFilter = ({
Please indicate a valid Swagger or OpenAPI version field.
- Supported version fields are swagger: "2.0" and
- those that match openapi: 3.x.y (for example,{" "}
- openapi: 3.1.0).
+ Supported version fields are swagger: "2.0",{" "}
+ openapi: 3.0.x, or openapi: 3.1.x (for
+ example, openapi: 3.1.0).
diff --git a/src/core/plugins/oas32/after-load.js b/src/core/plugins/oas32/after-load.js
new file mode 100644
index 00000000000..e192f818c30
--- /dev/null
+++ b/src/core/plugins/oas32/after-load.js
@@ -0,0 +1,58 @@
+/**
+ * @prettier
+ */
+import { makeIsFileUploadIntended } from "./oas3-extensions/fn"
+import { wrapOAS32Fn } from "./fn"
+import { immutableToJS } from "core/utils"
+
+function afterLoad({ fn, getSystem }) {
+ // Wire up JSON Schema 2020-12 sample generation functions for OAS 3.2.
+ // OAS 3.2 uses the same JSON Schema 2020-12 dialect as OAS 3.1, so we point
+ // the system-level sample functions at the jsonSchema202012 implementations
+ // when the loaded spec is OAS 3.2.
+ if (typeof fn.sampleFromSchema === "function" && fn.jsonSchema202012) {
+ const wrappedFns = wrapOAS32Fn(
+ {
+ sampleFromSchema: fn.jsonSchema202012.sampleFromSchema,
+ sampleFromSchemaGeneric: fn.jsonSchema202012.sampleFromSchemaGeneric,
+ createXMLExample: fn.jsonSchema202012.createXMLExample,
+ memoizedSampleFromSchema: fn.jsonSchema202012.memoizedSampleFromSchema,
+ memoizedCreateXMLExample: fn.jsonSchema202012.memoizedCreateXMLExample,
+ getJsonSampleSchema: fn.jsonSchema202012.getJsonSampleSchema,
+ getYamlSampleSchema: fn.jsonSchema202012.getYamlSampleSchema,
+ getXmlSampleSchema: fn.jsonSchema202012.getXmlSampleSchema,
+ getSampleSchema: fn.jsonSchema202012.getSampleSchema,
+ mergeJsonSchema: fn.jsonSchema202012.mergeJsonSchema,
+ getSchemaObjectTypeLabel: (schema) =>
+ fn.jsonSchema202012.getType(immutableToJS(schema)),
+ getSchemaObjectType: (schema) =>
+ fn.jsonSchema202012.foldType(immutableToJS(schema)?.type),
+ },
+ getSystem()
+ )
+
+ Object.assign(this.fn, wrappedFns)
+ }
+
+ // Wrap file-upload detection for OAS 3.2 (supports contentMediaType / contentEncoding).
+ const isFileUploadIntended = makeIsFileUploadIntended(getSystem)
+ const { isFileUploadIntended: isFileUploadIntendedWrap } = wrapOAS32Fn(
+ { isFileUploadIntended },
+ getSystem()
+ )
+
+ this.fn.isFileUploadIntended = isFileUploadIntendedWrap
+ this.fn.isFileUploadIntendedOAS32 = isFileUploadIntended
+
+ // Wrap hasSchemaType for OAS 3.2.
+ if (fn.jsonSchema202012) {
+ const { hasSchemaType } = wrapOAS32Fn(
+ { hasSchemaType: fn.jsonSchema202012.hasSchemaType },
+ getSystem()
+ )
+
+ this.fn.hasSchemaType = hasSchemaType
+ }
+}
+
+export default afterLoad
diff --git a/src/core/plugins/oas32/components/version-pragma-filter.jsx b/src/core/plugins/oas32/components/version-pragma-filter.jsx
new file mode 100644
index 00000000000..245ea0b9e3f
--- /dev/null
+++ b/src/core/plugins/oas32/components/version-pragma-filter.jsx
@@ -0,0 +1,80 @@
+/**
+ * @prettier
+ */
+import React from "react"
+import PropTypes from "prop-types"
+
+const VersionPragmaFilter = ({
+ bypass,
+ isSwagger2,
+ isOAS3,
+ isOAS31,
+ isOAS32,
+ alsoShow,
+ children,
+}) => {
+ if (bypass) {
+ return {children}
+ }
+
+ if (isSwagger2 && (isOAS3 || isOAS31 || isOAS32)) {
+ return (
+
+ {alsoShow}
+
+
+
Unable to render this definition
+
+ swagger and openapi fields cannot be
+ present in the same Swagger or OpenAPI definition. Please remove
+ one of the fields.
+
+
+ Supported version fields are swagger: "2.0"{" "}
+ and openapi: 3.0.x, openapi: 3.1.x, or{" "}
+ openapi: 3.2.x (for example,{" "}
+ openapi: 3.2.0).
+
+
+
+
+ )
+ }
+
+ if (!isSwagger2 && !isOAS3 && !isOAS31 && !isOAS32) {
+ return (
+
+ {alsoShow}
+
+
+
Unable to render this definition
+
+ The provided definition does not specify a valid version field.
+
+
+ Please indicate a valid Swagger or OpenAPI version field.
+ Supported version fields are swagger: "2.0"{" "}
+ and openapi: 3.0.x, openapi: 3.1.x, or{" "}
+ openapi: 3.2.x (for example,{" "}
+ openapi: 3.2.0).
+
+
+
+
+ )
+ }
+
+ return {children}
+}
+
+VersionPragmaFilter.propTypes = {
+ isSwagger2: PropTypes.bool.isRequired,
+ isOAS3: PropTypes.bool.isRequired,
+ isOAS31: PropTypes.bool.isRequired,
+ isOAS32: PropTypes.bool.isRequired,
+ bypass: PropTypes.bool,
+ alsoShow: PropTypes.element,
+ children: PropTypes.any,
+}
+
+export default VersionPragmaFilter
diff --git a/src/core/plugins/oas32/fn.js b/src/core/plugins/oas32/fn.js
new file mode 100644
index 00000000000..200efbe5b7e
--- /dev/null
+++ b/src/core/plugins/oas32/fn.js
@@ -0,0 +1,124 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+export const isOAS32 = (jsSpec) => {
+ const oasVersion = jsSpec.get("openapi")
+
+ return (
+ typeof oasVersion === "string" && /^3\.2\.(?:[1-9]\d*|0)$/.test(oasVersion)
+ )
+}
+
+/**
+ * Creates selector that returns value of the passed
+ * selector when spec is OpenAPI 3.2.x, null otherwise.
+ *
+ * @param selector
+ * @returns {function(*, ...[*]): function(*): (*|null)}
+ */
+export const createOnlyOAS32Selector =
+ (selector) =>
+ (state, ...args) =>
+ (system) => {
+ if (system.getSystem().specSelectors.isOAS32()) {
+ const selectedValue = selector(state, ...args)
+ return typeof selectedValue === "function"
+ ? selectedValue(system)
+ : selectedValue
+ } else {
+ return null
+ }
+ }
+
+/**
+ * Creates selector wrapper that returns value of the passed
+ * selector when spec is OpenAPI 3.2.x, calls original selector otherwise.
+ *
+ *
+ * @param selector
+ * @returns {function(*, *): function(*, ...[*]): (*)}
+ */
+export const createOnlyOAS32SelectorWrapper =
+ (selector) =>
+ (oriSelector, system) =>
+ (state, ...args) => {
+ if (system.getSystem().specSelectors.isOAS32()) {
+ const selectedValue = selector(state, ...args)
+ return typeof selectedValue === "function"
+ ? selectedValue(oriSelector, system)
+ : selectedValue
+ } else {
+ return oriSelector(...args)
+ }
+ }
+
+/**
+ * Creates selector that provides system as the
+ * second argument. This allows to create memoized
+ * composed selectors from different plugins.
+ *
+ * @param selector
+ * @returns {function(*, ...[*]): function(*): *}
+ */
+export const createSystemSelector =
+ (selector) =>
+ (state, ...args) =>
+ (system) => {
+ const selectedValue = selector(state, system, ...args)
+ return typeof selectedValue === "function"
+ ? selectedValue(system)
+ : selectedValue
+ }
+
+/* eslint-disable react/jsx-filename-extension */
+/**
+ * Creates component wrapper that only wraps the component
+ * when spec is OpenAPI 3.2.x. Otherwise, returns original
+ * component with passed props.
+ *
+ * @param Component
+ * @returns {function(*, *): function(*): *}
+ */
+export const createOnlyOAS32ComponentWrapper =
+ (Component) => (Original, system) => (props) => {
+ if (system.specSelectors.isOAS32()) {
+ return (
+
+ )
+ }
+
+ return
+ }
+/* eslint-enable react/jsx-filename-extension */
+
+/**
+ * Runs the fn replacement implementation when spec is OpenAPI 3.2.x.
+ * Runs the fn original implementation otherwise.
+ *
+ * @param fn
+ * @param system
+ * @returns {{[p: string]: function(...[*]): *}}
+ */
+export const wrapOAS32Fn = (fn, system) => {
+ const { fn: systemFn, specSelectors } = system
+
+ return Object.fromEntries(
+ Object.entries(fn).map(([name, newImpl]) => {
+ const oriImpl = systemFn[name]
+ const impl = (...args) =>
+ specSelectors.isOAS32()
+ ? newImpl(...args)
+ : typeof oriImpl === "function"
+ ? oriImpl(...args)
+ : undefined
+
+ return [name, impl]
+ })
+ )
+}
diff --git a/src/core/plugins/oas32/index.js b/src/core/plugins/oas32/index.js
new file mode 100644
index 00000000000..73fe1a6c36d
--- /dev/null
+++ b/src/core/plugins/oas32/index.js
@@ -0,0 +1,130 @@
+/**
+ * @prettier
+ */
+import VersionPragmaFilter from "./components/version-pragma-filter"
+import ContactWrapper from "./wrap-components/contact"
+import InfoWrapper from "./wrap-components/info"
+import LicenseWrapper from "./wrap-components/license"
+import ModelWrapper from "./wrap-components/model"
+import ModelsWrapper from "./wrap-components/models"
+import OpenAPIVersionWrapper from "./wrap-components/openapi-version"
+import VersionPragmaFilterWrapper from "./wrap-components/version-pragma-filter"
+import JSONSchema202012KeywordDescriptionWrapper from "./json-schema-2020-12-extensions/wrap-components/keywords/Description"
+import JSONSchema202012KeywordExamplesWrapper from "./json-schema-2020-12-extensions/wrap-components/keywords/Examples"
+import JSONSchema202012KeywordPropertiesWrapper from "./json-schema-2020-12-extensions/wrap-components/keywords/Properties"
+import {
+ isOAS32 as isOAS32Fn,
+ createOnlyOAS32Selector as createOnlyOAS32SelectorFn,
+ createSystemSelector as createSystemSelectorFn,
+} from "./fn"
+import { validOperationMethods } from "./selectors"
+import {
+ isOAS3 as isOAS3SelectorWrapper,
+ validOperationMethods as validOperationMethodsWrapper,
+} from "./spec-extensions/wrap-selectors"
+import {
+ license as selectLicense,
+ contact as selectContact,
+ selectIsOAS32,
+ selectLicenseNameField,
+ selectLicenseUrlField,
+ selectLicenseIdentifierField,
+ selectContactNameField,
+ selectContactEmailField,
+ selectContactUrlField,
+ selectContactUrl,
+ selectLicenseUrl,
+ selectInfoSummaryField,
+} from "./spec-extensions/selectors"
+import afterLoad from "./after-load"
+
+/**
+ * OpenAPI 3.2 Plugin
+ *
+ * Adds support for OpenAPI Specification 3.2.x
+ *
+ * This plugin should be loaded AFTER:
+ * - oas31 plugin
+ * - json-schema-2020-12 plugin (uses same as OAS 3.1)
+ *
+ * It wraps and overrides components/selectors from previous versions.
+ *
+ * New features in OAS 3.2 (basic implementation):
+ * - query operation: QUERY HTTP method support
+ * - info.summary: Short summary field in Info Object
+ *
+ * Additional features (not yet implemented):
+ * - $self: Self-referencing URI for base URI resolution
+ * - additionalOperations: Custom HTTP methods support
+ * - mediaTypes in Components: Reusable Media Type Objects
+ * - Tag enhancements (summary, kind, parent)
+ * - querystring parameter location
+ * - itemSchema for streaming responses
+ */
+const OAS32Plugin = ({ fn }) => {
+ const createSystemSelector = fn.createSystemSelector || createSystemSelectorFn
+
+ const plugin = {
+ afterLoad,
+ fn: {
+ isOAS32: isOAS32Fn,
+ createSystemSelector: createSystemSelectorFn,
+ createOnlyOAS32Selector: createOnlyOAS32SelectorFn,
+ },
+ components: {
+ OAS32VersionPragmaFilter: VersionPragmaFilter,
+ },
+ wrapComponents: {
+ Contact: ContactWrapper,
+ InfoContainer: InfoWrapper,
+ License: LicenseWrapper,
+ Model: ModelWrapper,
+ Models: ModelsWrapper,
+ OpenAPIVersion: OpenAPIVersionWrapper,
+ VersionPragmaFilter: VersionPragmaFilterWrapper,
+ JSONSchema202012KeywordDescription:
+ JSONSchema202012KeywordDescriptionWrapper,
+ JSONSchema202012KeywordExamples: JSONSchema202012KeywordExamplesWrapper,
+ JSONSchema202012KeywordProperties:
+ JSONSchema202012KeywordPropertiesWrapper,
+ },
+ statePlugins: {
+ spec: {
+ selectors: {
+ isOAS32: createSystemSelector(selectIsOAS32),
+
+ // Info selectors (inherited from OAS31)
+ selectInfoSummaryField,
+
+ // License and contact selectors (inherited from OAS31)
+ license: selectLicense,
+ selectLicenseNameField,
+ selectLicenseUrlField,
+ selectLicenseIdentifierField,
+ selectLicenseUrl: createSystemSelector(selectLicenseUrl),
+
+ contact: selectContact,
+ selectContactNameField,
+ selectContactEmailField,
+ selectContactUrlField,
+ selectContactUrl: createSystemSelector(selectContactUrl),
+ },
+ wrapSelectors: {
+ // Ensure OAS 3.2 specs are recognized as OAS 3.x (for servers, etc.)
+ isOAS3: isOAS3SelectorWrapper,
+ // Override validOperationMethods to include QUERY for OAS 3.2
+ validOperationMethods: validOperationMethodsWrapper,
+ },
+ },
+ oas32: {
+ selectors: {
+ validOperationMethods,
+ },
+ },
+ },
+ }
+
+ return plugin
+}
+
+export default OAS32Plugin
diff --git a/src/core/plugins/oas32/json-schema-2020-12-extensions/components/keywords/Description.jsx b/src/core/plugins/oas32/json-schema-2020-12-extensions/components/keywords/Description.jsx
new file mode 100644
index 00000000000..36c171a4eac
--- /dev/null
+++ b/src/core/plugins/oas32/json-schema-2020-12-extensions/components/keywords/Description.jsx
@@ -0,0 +1,27 @@
+/**
+ * @prettier
+ */
+import React from "react"
+import PropTypes from "prop-types"
+
+const Description = ({ schema, getSystem }) => {
+ if (!schema?.description) return null
+
+ const { getComponent } = getSystem()
+ const MarkDown = getComponent("Markdown")
+
+ return (
+
+ )
+}
+
+Description.propTypes = {
+ schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,
+ getSystem: PropTypes.func.isRequired,
+}
+
+export default Description
diff --git a/src/core/plugins/oas32/json-schema-2020-12-extensions/components/keywords/Properties.jsx b/src/core/plugins/oas32/json-schema-2020-12-extensions/components/keywords/Properties.jsx
new file mode 100644
index 00000000000..733b8dd8c3d
--- /dev/null
+++ b/src/core/plugins/oas32/json-schema-2020-12-extensions/components/keywords/Properties.jsx
@@ -0,0 +1,60 @@
+/**
+ * @prettier
+ */
+import React from "react"
+import PropTypes from "prop-types"
+import classNames from "classnames"
+
+const Properties = ({ schema, getSystem }) => {
+ const { fn, getComponent } = getSystem()
+ const { useComponent, usePath } = fn.jsonSchema202012
+ const { getDependentRequired, getProperties } = fn.jsonSchema202012.useFn()
+ const config = fn.jsonSchema202012.useConfig()
+ const required = Array.isArray(schema?.required) ? schema.required : []
+ const { path } = usePath("properties")
+ const JSONSchema = useComponent("JSONSchema")
+ const JSONSchemaPathContext = getComponent("JSONSchema202012PathContext")()
+ const properties = getProperties(schema, config)
+
+ /**
+ * Rendering.
+ */
+ if (Object.keys(properties).length === 0) {
+ return null
+ }
+
+ return (
+
+
+
+ {Object.entries(properties).map(([propertyName, propertySchema]) => {
+ const isRequired = required.includes(propertyName)
+ const dependentRequired = getDependentRequired(propertyName, schema)
+
+ return (
+ -
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+Properties.propTypes = {
+ schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,
+ getSystem: PropTypes.func.isRequired,
+}
+
+export default Properties
diff --git a/src/core/plugins/oas32/json-schema-2020-12-extensions/fn.js b/src/core/plugins/oas32/json-schema-2020-12-extensions/fn.js
new file mode 100644
index 00000000000..48e2e927a73
--- /dev/null
+++ b/src/core/plugins/oas32/json-schema-2020-12-extensions/fn.js
@@ -0,0 +1,20 @@
+/**
+ * @prettier
+ */
+export const makeGetSchemaKeywords = (original) => {
+ if (typeof original !== "function") {
+ return null
+ }
+
+ const jsonSchema202012Keywords = original()
+
+ return () => [
+ ...jsonSchema202012Keywords,
+ "discriminator",
+ "xml",
+ "externalDocs",
+ "example",
+ // $$ref is an internal keyword used for dereferencing
+ "$$ref",
+ ]
+}
diff --git a/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Description.jsx b/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Description.jsx
new file mode 100644
index 00000000000..7b864a9597d
--- /dev/null
+++ b/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Description.jsx
@@ -0,0 +1,9 @@
+/**
+ * @prettier
+ */
+import DescriptionKeyword from "../../components/keywords/Description"
+import { createOnlyOAS32ComponentWrapper } from "../../../fn"
+
+const DescriptionWrapper = createOnlyOAS32ComponentWrapper(DescriptionKeyword)
+
+export default DescriptionWrapper
diff --git a/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Examples.jsx b/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Examples.jsx
new file mode 100644
index 00000000000..90a9e94d0af
--- /dev/null
+++ b/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Examples.jsx
@@ -0,0 +1,31 @@
+/**
+ * @prettier
+ */
+import React from "react"
+import { createOnlyOAS32ComponentWrapper } from "../../../fn"
+
+const ExamplesWrapper = createOnlyOAS32ComponentWrapper(
+ ({ schema, getSystem, originalComponent: KeywordExamples }) => {
+ const { getComponent } = getSystem()
+ const KeywordDiscriminator = getComponent(
+ "JSONSchema202012KeywordDiscriminator"
+ )
+ const KeywordXml = getComponent("JSONSchema202012KeywordXml")
+ const KeywordExample = getComponent("JSONSchema202012KeywordExample")
+ const KeywordExternalDocs = getComponent(
+ "JSONSchema202012KeywordExternalDocs"
+ )
+
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+ }
+)
+
+export default ExamplesWrapper
diff --git a/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Properties.jsx b/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Properties.jsx
new file mode 100644
index 00000000000..f48b2c859b8
--- /dev/null
+++ b/src/core/plugins/oas32/json-schema-2020-12-extensions/wrap-components/keywords/Properties.jsx
@@ -0,0 +1,9 @@
+/**
+ * @prettier
+ */
+import PropertiesKeyword from "../../components/keywords/Properties"
+import { createOnlyOAS32ComponentWrapper } from "../../../fn"
+
+const PropertiesWrapper = createOnlyOAS32ComponentWrapper(PropertiesKeyword)
+
+export default PropertiesWrapper
diff --git a/src/core/plugins/oas32/oas3-extensions/fn.js b/src/core/plugins/oas32/oas3-extensions/fn.js
new file mode 100644
index 00000000000..db7d801704a
--- /dev/null
+++ b/src/core/plugins/oas32/oas3-extensions/fn.js
@@ -0,0 +1,46 @@
+/**
+ * @prettier
+ */
+import { Map } from "immutable"
+import isPlainObject from "lodash/isPlainObject"
+
+export const makeIsFileUploadIntended = (getSystem) => {
+ const isFileUploadIntended = (schema, mediaType = null) => {
+ const { fn } = getSystem()
+
+ /**
+ * Return `true` early if the media type indicates a file upload
+ * or if a combination of type: `string` and format: `binary/byte` is detected.
+ * This ensures support for empty Media Type Objects,
+ * as the schema check is performed later.
+ */
+ const isFileUploadIntendedOAS30 = fn.isFileUploadIntendedOAS30(
+ schema,
+ mediaType
+ )
+
+ if (isFileUploadIntendedOAS30) {
+ return true
+ }
+
+ const isSchemaImmutable = Map.isMap(schema)
+
+ if (!isSchemaImmutable && !isPlainObject(schema)) {
+ return false
+ }
+
+ const contentMediaType = isSchemaImmutable
+ ? schema.get("contentMediaType")
+ : schema.contentMediaType
+ const contentEncoding = isSchemaImmutable
+ ? schema.get("contentEncoding")
+ : schema.contentEncoding
+
+ return (
+ (typeof contentMediaType === "string" && contentMediaType !== "") ||
+ (typeof contentEncoding === "string" && contentEncoding !== "")
+ )
+ }
+
+ return isFileUploadIntended
+}
diff --git a/src/core/plugins/oas32/selectors.js b/src/core/plugins/oas32/selectors.js
new file mode 100644
index 00000000000..59fbd4d7068
--- /dev/null
+++ b/src/core/plugins/oas32/selectors.js
@@ -0,0 +1,24 @@
+/**
+ * @prettier
+ */
+import constant from "lodash/constant"
+
+/**
+ * Valid HTTP operation methods for OAS 3.2.x
+ *
+ * OAS 3.2.0 adds support for the QUERY HTTP method per
+ * draft-ietf-httpbis-safe-method-w-body
+ *
+ * Reference: https://spec.openapis.org/oas/v3.2.0.html#path-item-object
+ */
+export const validOperationMethods = constant([
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ "query", // NEW in OAS 3.2
+])
diff --git a/src/core/plugins/oas32/spec-extensions/selectors.js b/src/core/plugins/oas32/spec-extensions/selectors.js
new file mode 100644
index 00000000000..2eee9207143
--- /dev/null
+++ b/src/core/plugins/oas32/spec-extensions/selectors.js
@@ -0,0 +1,86 @@
+/**
+ * @prettier
+ */
+import { Map } from "immutable"
+import { createSelector } from "reselect"
+
+import { safeBuildUrl } from "core/utils/url"
+import { isOAS32 } from "../fn"
+
+const map = Map()
+
+/**
+ * Detects if the current spec is OAS 3.2.x
+ */
+export const selectIsOAS32 = (state, system) => () => {
+ const spec = system.specSelectors.specJson()
+ return isOAS32(spec)
+}
+
+export const license = () => (system) => {
+ const license = system.specSelectors.info().get("license")
+ return Map.isMap(license) ? license : map
+}
+
+export const selectLicenseNameField = () => (system) => {
+ return system.specSelectors.license().get("name", "License")
+}
+
+export const selectLicenseUrlField = () => (system) => {
+ return system.specSelectors.license().get("url")
+}
+
+export const selectLicenseUrl = createSelector(
+ [
+ (state, system) => system.specSelectors.url(),
+ (state, system) => system.oas3Selectors.selectedServer(),
+ (state, system) => system.specSelectors.selectLicenseUrlField(),
+ ],
+ (specUrl, selectedServer, url) => {
+ if (url) {
+ return safeBuildUrl(url, specUrl, { selectedServer })
+ }
+
+ return undefined
+ }
+)
+
+export const selectLicenseIdentifierField = () => (system) => {
+ return system.specSelectors.license().get("identifier")
+}
+
+export const contact = () => (system) => {
+ const contact = system.specSelectors.info().get("contact")
+ return Map.isMap(contact) ? contact : map
+}
+
+export const selectContactNameField = () => (system) => {
+ return system.specSelectors.contact().get("name", "the developer")
+}
+
+export const selectContactEmailField = () => (system) => {
+ return system.specSelectors.contact().get("email")
+}
+
+export const selectContactUrlField = () => (system) => {
+ return system.specSelectors.contact().get("url")
+}
+
+export const selectContactUrl = createSelector(
+ [
+ (state, system) => system.specSelectors.url(),
+ (state, system) => system.oas3Selectors.selectedServer(),
+ (state, system) => system.specSelectors.selectContactUrlField(),
+ ],
+ (specUrl, selectedServer, url) => {
+ if (url) {
+ return safeBuildUrl(url, specUrl, { selectedServer })
+ }
+
+ return undefined
+ }
+)
+
+export const selectInfoSummaryField = () => (system) => {
+ return system.specSelectors.info().get("summary")
+}
diff --git a/src/core/plugins/oas32/spec-extensions/wrap-selectors.js b/src/core/plugins/oas32/spec-extensions/wrap-selectors.js
new file mode 100644
index 00000000000..da9e4a82438
--- /dev/null
+++ b/src/core/plugins/oas32/spec-extensions/wrap-selectors.js
@@ -0,0 +1,27 @@
+/**
+ * @prettier
+ */
+import { createOnlyOAS32SelectorWrapper } from "../fn"
+
+/**
+ * Wraps isOAS3 selector to return true when spec is OAS 3.2.x
+ * This ensures OAS 3.2 specs are recognized as OAS 3.x for
+ * OAS3-specific features like servers, security, etc.
+ */
+export const isOAS3 =
+ (oriSelector, system) =>
+ (state, ...args) => {
+ const isOAS32 = system.specSelectors.isOAS32()
+ return isOAS32 || oriSelector(...args)
+ }
+
+/**
+ * Wraps validOperationMethods to include QUERY method for OAS 3.2.x
+ * OAS 3.2.0 adds support for the QUERY HTTP method per
+ * draft-ietf-httpbis-safe-method-w-body
+ *
+ * Reference: https://spec.openapis.org/oas/v3.2.0.html#path-item-object
+ */
+export const validOperationMethods = createOnlyOAS32SelectorWrapper(
+ () => (oriSelector, system) => system.oas32Selectors.validOperationMethods()
+)
diff --git a/src/core/plugins/oas32/wrap-components/contact.jsx b/src/core/plugins/oas32/wrap-components/contact.jsx
new file mode 100644
index 00000000000..2a16e213eda
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/contact.jsx
@@ -0,0 +1,13 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+import { createOnlyOAS32ComponentWrapper } from "../fn"
+
+export default createOnlyOAS32ComponentWrapper((props) => {
+ const { getSystem } = props
+ const system = getSystem()
+ const OAS31Contact = system.getComponent("OAS31Contact", true)
+ return
+})
diff --git a/src/core/plugins/oas32/wrap-components/info.jsx b/src/core/plugins/oas32/wrap-components/info.jsx
new file mode 100644
index 00000000000..e6d54ce3df1
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/info.jsx
@@ -0,0 +1,15 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+import { createOnlyOAS32ComponentWrapper } from "../fn"
+
+const InfoWrapper = createOnlyOAS32ComponentWrapper(({ getSystem }) => {
+ const system = getSystem()
+ const OAS31Info = system.getComponent("OAS31Info", true)
+
+ return
+})
+
+export default InfoWrapper
diff --git a/src/core/plugins/oas32/wrap-components/license.jsx b/src/core/plugins/oas32/wrap-components/license.jsx
new file mode 100644
index 00000000000..246e004d73a
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/license.jsx
@@ -0,0 +1,13 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+import { createOnlyOAS32ComponentWrapper } from "../fn"
+
+export default createOnlyOAS32ComponentWrapper((props) => {
+ const { getSystem } = props
+ const system = getSystem()
+ const OAS31License = system.getComponent("OAS31License", true)
+ return
+})
diff --git a/src/core/plugins/oas32/wrap-components/model.jsx b/src/core/plugins/oas32/wrap-components/model.jsx
new file mode 100644
index 00000000000..c4ca0416ec6
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/model.jsx
@@ -0,0 +1,44 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+import { createOnlyOAS32ComponentWrapper } from "../fn"
+import { makeGetSchemaKeywords } from "../json-schema-2020-12-extensions/fn"
+
+const ModelWrapper = createOnlyOAS32ComponentWrapper(
+ ({ getSystem, ...props }) => {
+ const system = getSystem()
+ const { getComponent, fn, getConfigs } = system
+ const configs = getConfigs()
+
+ const Model = getComponent("OAS31Model")
+ const withJSONSchemaSystemContext = getComponent(
+ "withJSONSchema202012SystemContext"
+ )
+
+ // we cache the HOC as recreating it with every re-render is quite expensive
+ ModelWrapper.ModelWithJSONSchemaContext ??= withJSONSchemaSystemContext(
+ Model,
+ {
+ config: {
+ default$schema: "https://spec.openapis.org/oas/3.2/schema/2025-09-17",
+ defaultExpandedLevels: configs.defaultModelExpandDepth,
+ includeReadOnly: props.includeReadOnly,
+ includeWriteOnly: props.includeWriteOnly,
+ },
+ fn: {
+ getProperties: fn.jsonSchema202012.getProperties,
+ isExpandable: fn.jsonSchema202012.isExpandable,
+ getSchemaKeywords: makeGetSchemaKeywords(
+ fn.jsonSchema202012.getSchemaKeywords
+ ),
+ },
+ }
+ )
+
+ return
+ }
+)
+
+export default ModelWrapper
diff --git a/src/core/plugins/oas32/wrap-components/models.jsx b/src/core/plugins/oas32/wrap-components/models.jsx
new file mode 100644
index 00000000000..1a364c76eec
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/models.jsx
@@ -0,0 +1,47 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+import { createOnlyOAS32ComponentWrapper } from "../fn"
+import { makeGetSchemaKeywords } from "../json-schema-2020-12-extensions/fn"
+
+const ModelsWrapper = createOnlyOAS32ComponentWrapper(({ getSystem }) => {
+ const { getComponent, fn, getConfigs } = getSystem()
+ const configs = getConfigs()
+
+ if (ModelsWrapper.ModelsWithJSONSchemaContext) {
+ return
+ }
+
+ const Models = getComponent("OAS31Models", true)
+ const withJSONSchemaSystemContext = getComponent(
+ "withJSONSchema202012SystemContext"
+ )
+
+ // we cache the HOC as recreating it with every re-render is quite expensive
+ ModelsWrapper.ModelsWithJSONSchemaContext ??= withJSONSchemaSystemContext(
+ Models,
+ {
+ config: {
+ default$schema: "https://spec.openapis.org/oas/3.2/schema/2025-09-17",
+ defaultExpandedLevels: configs.defaultModelsExpandDepth - 1,
+ includeReadOnly: true,
+ includeWriteOnly: true,
+ },
+ fn: {
+ getProperties: fn.jsonSchema202012.getProperties,
+ isExpandable: fn.jsonSchema202012.isExpandable,
+ getSchemaKeywords: makeGetSchemaKeywords(
+ fn.jsonSchema202012.getSchemaKeywords
+ ),
+ },
+ }
+ )
+
+ return
+})
+
+ModelsWrapper.ModelsWithJSONSchemaContext = null
+
+export default ModelsWrapper
diff --git a/src/core/plugins/oas32/wrap-components/openapi-version.jsx b/src/core/plugins/oas32/wrap-components/openapi-version.jsx
new file mode 100644
index 00000000000..43cd423aadb
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/openapi-version.jsx
@@ -0,0 +1,11 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+import { createOnlyOAS32ComponentWrapper } from "../fn"
+
+export default createOnlyOAS32ComponentWrapper((props) => {
+ const { originalComponent: Ori } = props
+ return
+})
diff --git a/src/core/plugins/oas32/wrap-components/version-pragma-filter.jsx b/src/core/plugins/oas32/wrap-components/version-pragma-filter.jsx
new file mode 100644
index 00000000000..1b1c83501b8
--- /dev/null
+++ b/src/core/plugins/oas32/wrap-components/version-pragma-filter.jsx
@@ -0,0 +1,16 @@
+/**
+ * @prettier
+ */
+import React from "react"
+
+const VersionPragmaFilterWrapper = (Original, system) => (props) => {
+ const isOAS32 = system.specSelectors.isOAS32()
+
+ const OAS32VersionPragmaFilter = system.getComponent(
+ "OAS32VersionPragmaFilter"
+ )
+
+ return
+}
+
+export default VersionPragmaFilterWrapper
diff --git a/src/core/plugins/spec/selectors.js b/src/core/plugins/spec/selectors.js
index 3a0d7f4857a..ada3b9ba8b8 100644
--- a/src/core/plugins/spec/selectors.js
+++ b/src/core/plugins/spec/selectors.js
@@ -6,7 +6,7 @@ import { fromJS, Set, Map, OrderedMap, List } from "immutable"
const DEFAULT_TAG = "default"
const OPERATION_METHODS = [
- "get", "put", "post", "delete", "options", "head", "patch", "trace"
+ "get", "put", "post", "delete", "options", "head", "patch", "trace", "query"
]
const state = state => {
diff --git a/src/core/presets/apis/index.js b/src/core/presets/apis/index.js
index 74d0e8022d4..239526f29b7 100644
--- a/src/core/presets/apis/index.js
+++ b/src/core/presets/apis/index.js
@@ -4,6 +4,7 @@
import BasePreset from "core/presets/base"
import OpenAPI30Plugin from "core/plugins/oas3"
import OpenAPI31Plugin from "core/plugins/oas31"
+import OpenAPI32Plugin from "core/plugins/oas32"
import JSONSchema202012Plugin from "core/plugins/json-schema-2020-12"
import JSONSchema202012SamplesPlugin from "core/plugins/json-schema-2020-12-samples"
@@ -14,5 +15,6 @@ export default function PresetApis() {
JSONSchema202012Plugin,
JSONSchema202012SamplesPlugin,
OpenAPI31Plugin,
+ OpenAPI32Plugin, // Load LAST to override previous versions
]
}
diff --git a/src/style/_dark-mode.scss b/src/style/_dark-mode.scss
index ffa453fdbd3..397cecada66 100644
--- a/src/style/_dark-mode.scss
+++ b/src/style/_dark-mode.scss
@@ -1,41 +1,87 @@
@use "sass:list";
// Variables for consistent theme
-$neutral-10: #F0F1F1;
-$neutral-20: #E4E6E6;
-$neutral-30: #D2D6D7;
-$neutral-40: #B7BCBF;
-$neutral-50: #8C969A;
-$neutral-60: #6B757A;
-$neutral-80: #545D61;
-$neutral-85: #434B4F;
-$neutral-95: #2A2E30;
-$neutral-98: #1C2022;
-$neutral-100: #080A0B;
-
-$success-30: #4AC966;
-$error-30: #FF5F5F;
-
-$authorize-button: #3ECE90;
-$link: #51A8FF;
-$markdown-code: #B68AE1;
-$textarea: #0D1014;
+$neutral-10: #f0f1f1;
+$neutral-20: #e4e6e6;
+$neutral-30: #d2d6d7;
+$neutral-40: #b7bcbf;
+$neutral-50: #8c969a;
+$neutral-60: #6b757a;
+$neutral-80: #545d61;
+$neutral-85: #434b4f;
+$neutral-95: #2a2e30;
+$neutral-98: #1c2022;
+$neutral-100: #080a0b;
+
+$success-30: #4ac966;
+$error-30: #ff5f5f;
+
+$authorize-button: #3ece90;
+$link: #51a8ff;
+$markdown-code: #b68ae1;
+$textarea: #0d1014;
$textarea-disabled: #202225;
-$info-version-stamp: #1D632E;
-$curl-command-button: #3B424D;
-$curl-command-button-text: #EBEBEB;
-$json-schema-2020-12__attribute--primary: #9898FF;
-$json-schema-2020-12__constraint--string: #D4AA53;
+$info-version-stamp: #1d632e;
+$curl-command-button: #3b424d;
+$curl-command-button-text: #ebebeb;
+$json-schema-2020-12__attribute--primary: #9898ff;
+$json-schema-2020-12__constraint--string: #d4aa53;
$opblock_colors: (
- post: (#112929, #104834, #14392C, #00B572),
- deprecated: (#272C34, #495361, #262E36, #6A6A6A),
- put: (#27201E, #523524, #9a5b3e, #FF7D35),
- get: (#182536, #294262, #1C3043, #55A1FF),
- delete: (#241A20, #4B2420, #2F2020, #EB6156),
- patch: (#11282F, #16494B, #113239, #03B7BF),
- head: (#282231, #44336A, #352C45, #B889FF),
- options: (#202C3C, #33465E, #314558, #6895C8),
+ post: (
+ #112929,
+ #104834,
+ #14392c,
+ #00b572,
+ ),
+ deprecated: (
+ #272c34,
+ #495361,
+ #262e36,
+ #6a6a6a,
+ ),
+ put: (
+ #27201e,
+ #523524,
+ #9a5b3e,
+ #ff7d35,
+ ),
+ get: (
+ #182536,
+ #294262,
+ #1c3043,
+ #55a1ff,
+ ),
+ delete: (
+ #241a20,
+ #4b2420,
+ #2f2020,
+ #eb6156,
+ ),
+ patch: (
+ #11282f,
+ #16494b,
+ #113239,
+ #03b7bf,
+ ),
+ head: (
+ #282231,
+ #44336a,
+ #352c45,
+ #b889ff,
+ ),
+ options: (
+ #202c3c,
+ #33465e,
+ #314558,
+ #6895c8,
+ ),
+ query: (
+ #2a1a28,
+ #4a2848,
+ #3a2238,
+ #d977c6,
+ ),
);
html.dark-mode {
@@ -57,13 +103,16 @@ html.dark-mode {
}
table thead tr {
- td, th {
+ td,
+ th {
color: $neutral-20;
}
}
- .markdown, .renderedMarkdown {
- p, pre {
+ .markdown,
+ .renderedMarkdown {
+ p,
+ pre {
color: $neutral-20;
}
@@ -109,7 +158,7 @@ html.dark-mode {
right 10px center no-repeat;
color: $neutral-10;
border-color: $neutral-40;
- box-shadow: none;
+ box-shadow: none;
outline: none;
&[multiple] {
@@ -121,12 +170,15 @@ html.dark-mode {
}
}
- input::placeholder, textarea::placeholder {
+ input::placeholder,
+ textarea::placeholder {
color: $neutral-10;
opacity: 0.5;
}
- input.invalid, select.invalid, textarea.invalid {
+ input.invalid,
+ select.invalid,
+ textarea.invalid {
background: $neutral-98;
border-color: $error-30;
}
@@ -158,17 +210,22 @@ html.dark-mode {
.modal-ux {
background-color: $neutral-95;
color: $neutral-20;
- border: none;
+ border: none;
&-header {
border-color: $neutral-80;
- .close-modal svg {
+ .close-modal svg {
fill: $neutral-20;
}
}
- h2, h3, h4, h5, p, label {
+ h2,
+ h3,
+ h4,
+ h5,
+ p,
+ label {
color: $neutral-20;
}
@@ -207,8 +264,8 @@ html.dark-mode {
}
}
}
-
- // ------ LOADING SPINNER ------
+
+ // ------ LOADING SPINNER ------
.loading-container .loading {
&::before {
@@ -221,7 +278,7 @@ html.dark-mode {
}
}
- // ------ SCHEMES / SERVERS ------
+ // ------ SCHEMES / SERVERS ------
.scheme-container {
background: $neutral-98;
@@ -241,14 +298,22 @@ html.dark-mode {
}
}
- // ------ INFO ------
+ // ------ INFO ------
.info {
- h1, h2, h3, h4, h5, .title {
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ .title {
color: $neutral-30;
}
- li, p, table, .base-url {
+ li,
+ p,
+ table,
+ .base-url {
color: $neutral-20;
}
@@ -268,7 +333,8 @@ html.dark-mode {
background: $neutral-85;
border-color: $error-30;
- h4, span {
+ h4,
+ span {
color: $neutral-20;
}
@@ -281,7 +347,8 @@ html.dark-mode {
// ------ COPY / DOWNLOAD BUTTONS ------
- .copy-to-clipboard, .download-contents {
+ .copy-to-clipboard,
+ .download-contents {
background: $neutral-80;
color: $neutral-20;
@@ -476,7 +543,8 @@ html.dark-mode {
}
.responses-inner {
- h4, h5 {
+ h4,
+ h5 {
color: $neutral-20;
}
}
@@ -507,7 +575,12 @@ html.dark-mode {
color: $markdown-code;
}
- .property-row, .brace-open, .brace-close, .prop-format, .property, .description {
+ .property-row,
+ .brace-open,
+ .brace-close,
+ .prop-format,
+ .property,
+ .description {
color: $neutral-20;
}
@@ -519,7 +592,8 @@ html.dark-mode {
.model-box {
background: $neutral-95;
- .model-title, .model {
+ .model-title,
+ .model {
color: $neutral-20;
}
@@ -527,7 +601,7 @@ html.dark-mode {
&:focus {
outline: none;
}
-
+
&:not(.prop) {
color: $neutral-20;
}
@@ -555,7 +629,10 @@ html.dark-mode {
color: $neutral-20;
}
- &-property--required > .json-schema-2020-12:first-of-type > .json-schema-2020-12-head .json-schema-2020-12__title::after {
+ &-property--required
+ > .json-schema-2020-12:first-of-type
+ > .json-schema-2020-12-head
+ .json-schema-2020-12__title::after {
color: $error-30;
}
@@ -596,10 +673,10 @@ html.dark-mode {
}
&--patternProperties {
- .json-schema-2020-12__title::before,
+ .json-schema-2020-12__title::before,
.json-schema-2020-12__title::after {
color: $json-schema-2020-12__attribute--primary;
- }
+ }
}
}
@@ -618,7 +695,8 @@ html.dark-mode {
}
&-json-viewer {
- &__name--secondary, &__value--secondary {
+ &__name--secondary,
+ &__value--secondary {
color: $neutral-40;
}
}
diff --git a/src/style/_layout.scss b/src/style/_layout.scss
index b6f3ba7727f..294e9a8f92f 100644
--- a/src/style/_layout.scss
+++ b/src/style/_layout.scss
@@ -388,6 +388,10 @@
@include mixins.method($color-options);
}
+ &.opblock-query {
+ @include mixins.method($color-query);
+ }
+
&.opblock-deprecated {
opacity: 0.6;
diff --git a/src/style/_topbar.scss b/src/style/_topbar.scss
index edbae35d96b..c5fbf8bf4ec 100644
--- a/src/style/_topbar.scss
+++ b/src/style/_topbar.scss
@@ -106,7 +106,7 @@
.dark-mode-toggle {
margin-left: 10px;
opacity: 0.8;
- transition: all .2s;
+ transition: all 0.2s;
cursor: pointer;
button {
@@ -115,7 +115,7 @@
padding: 0;
svg {
- fill: #E4E6E6;
+ fill: #e4e6e6;
}
}
diff --git a/src/style/_variables.scss b/src/style/_variables.scss
index 771e564d3fb..d297d4a6e0d 100644
--- a/src/style/_variables.scss
+++ b/src/style/_variables.scss
@@ -53,6 +53,7 @@ $color-head: #9012fe !default;
$color-patch: #50e3c2 !default;
$color-disabled: #ebebeb !default;
$color-options: #0d5aa7 !default;
+$color-query: #9d408a !default; // OAS 3.2 QUERY method
// Authorize
diff --git a/test/e2e-cypress/e2e/features/oas-badge.cy.js b/test/e2e-cypress/e2e/features/oas-badge.cy.js
index 1b70c906fc0..8656d915a5a 100644
--- a/test/e2e-cypress/e2e/features/oas-badge.cy.js
+++ b/test/e2e-cypress/e2e/features/oas-badge.cy.js
@@ -22,4 +22,12 @@ describe("OpenAPI Badge", () => {
.get("pre.version")
.contains("OAS 3.1")
})
+
+ it("should display light green badge with version indicator for OpenAPI 3.2.0", () => {
+ cy.visit("/?url=/documents/oas32/oas32-features.yaml")
+ .get("#swagger-ui")
+ .get('*[class^="version-stamp"]')
+ .get("pre.version")
+ .contains("OAS 3.2")
+ })
})
diff --git a/test/e2e-cypress/e2e/features/oas32-contact-and-license.cy.js b/test/e2e-cypress/e2e/features/oas32-contact-and-license.cy.js
new file mode 100644
index 00000000000..0c7aca1c21e
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/oas32-contact-and-license.cy.js
@@ -0,0 +1,34 @@
+/**
+ * @prettier
+ */
+describe("Render Contact and License in OAS 3.2.0", () => {
+ const baseUrl = "/?url=/documents/oas32/contact-and-license.yaml"
+
+ it("should render contact with all fields", () => {
+ cy.visit(baseUrl)
+ .get(".info__contact")
+ .should("exist")
+ .find("a")
+ .first()
+ .should("contain.text", "API Support Team")
+ .should("have.attr", "href", "https://www.example.com/support")
+ .should("have.attr", "rel")
+ .and("include", "noopener")
+ })
+
+ it("should render license with all fields", () => {
+ cy.visit(baseUrl)
+ .get(".info__license")
+ .should("exist")
+ .get(".info__license__url")
+ .should("contain.text", "Apache 2.0")
+ .find("a")
+ .should(
+ "have.attr",
+ "href",
+ "https://www.apache.org/licenses/LICENSE-2.0.html"
+ )
+ .should("have.attr", "rel")
+ .and("include", "noopener")
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/oas32-extension.cy.js b/test/e2e-cypress/e2e/features/oas32-extension.cy.js
new file mode 100644
index 00000000000..373bb2ff08b
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/oas32-extension.cy.js
@@ -0,0 +1,78 @@
+/**
+ * @prettier
+ */
+
+const showsExtensions = (keyword) => {
+ it("extensions are visible on keyword click", () => {
+ cy.get(".json-schema-2020-12-json-viewer__name")
+ .contains("x-primitiveExtension")
+ .should("not.be.visible")
+ cy.get(".json-schema-2020-12-json-viewer__name")
+ .contains("x-arrayExtension")
+ .should("not.be.visible")
+ cy.get(".json-schema-2020-12-json-viewer__name")
+ .contains("x-objectExtension")
+ .should("not.be.visible")
+
+ cy.get(".json-schema-2020-12-keyword__name").contains(keyword).click()
+
+ cy.get(".json-schema-2020-12-json-viewer__name")
+ .contains("x-primitiveExtension")
+ .should("be.visible")
+ cy.get(".json-schema-2020-12-json-viewer__name")
+ .contains("x-arrayExtension")
+ .should("be.visible")
+ cy.get(".json-schema-2020-12-json-viewer__name")
+ .contains("x-objectExtension")
+ .should("be.visible")
+ })
+}
+
+describe("OpenAPI 3.2 extension keyword", () => {
+ describe("displays extensions", () => {
+ beforeEach(() => {
+ cy.visit(
+ "/?url=/documents/features/oas32-extension.yaml&showExtensions=true"
+ )
+ })
+
+ describe("Discriminator extension", () => {
+ beforeEach(() => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ })
+ showsExtensions("Discriminator")
+ })
+
+ describe("External documentation extension", () => {
+ beforeEach(() => {
+ cy.get(".json-schema-2020-12").contains("Object").click()
+ })
+ showsExtensions("External documentation")
+ })
+
+ describe("XML extension", () => {
+ beforeEach(() => {
+ cy.get(".json-schema-2020-12").contains("Book").click()
+ })
+ showsExtensions("XML")
+ })
+ })
+
+ it("should hide extensions if showExtensions option is set to false", () => {
+ cy.visit(
+ "/?url=/documents/features/oas32-extension.yaml&showExtensions=false"
+ )
+ cy.get(".json-schema-2020-12").contains("Object").click()
+ cy.get(".json-schema-2020-12-keyword__name")
+ .contains("External documentation")
+ .click()
+
+ cy.get(".json-schema-2020-12-keyword__name--secondary")
+ .contains("url")
+ .should("be.visible")
+
+ cy.contains("x-primitiveExtension").should("not.exist")
+ cy.contains("x-arrayExtension").should("not.exist")
+ cy.contains("x-objectExtension").should("not.exist")
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/oas32-query-operation.cy.js b/test/e2e-cypress/e2e/features/oas32-query-operation.cy.js
new file mode 100644
index 00000000000..b79247b783a
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/oas32-query-operation.cy.js
@@ -0,0 +1,30 @@
+/**
+ * @prettier
+ */
+
+describe("OpenAPI 3.2 QUERY operation rendering", () => {
+ const baseUrl = "/?url=/documents/features/oas32-query-operation.yaml"
+
+ it("should render QUERY operation with all fields", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .should("exist")
+ .find(".opblock-summary-method")
+ .should("contain", "QUERY")
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .should("have.class", "is-open")
+ .find(".opblock-body")
+ .should("exist")
+ })
+
+ it("should render multiple operations including QUERY", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchProducts")
+ .should("exist")
+ .get("#operations-Search-advancedSearchProducts")
+ .should("exist")
+ .get("#operations-Search-searchWithQuery")
+ .should("exist")
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/oas32/oas32-component-only.cy.js b/test/e2e-cypress/e2e/features/oas32/oas32-component-only.cy.js
new file mode 100644
index 00000000000..2c8fbb712c8
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/oas32/oas32-component-only.cy.js
@@ -0,0 +1,21 @@
+/**
+ * @prettier
+ */
+
+describe("OpenAPI 3.2.0 - Component-Only Specification", () => {
+ const baseUrl = "/?url=/documents/oas32/component-only.yaml"
+
+ it("should render component-only spec with all fields", () => {
+ cy.visit(baseUrl)
+ .get(".version-pragma__message--missing")
+ .should("not.exist")
+ .get(".information-container")
+ .should("exist")
+ .find(".title")
+ .should("contain", "Component-Only Specification")
+ .get(".information-container .description")
+ .should("contain", "valid OAS 3.2.0 specification")
+ .get(".opblock-tag-section")
+ .should("not.exist")
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/oas32/oas32-query-operation.cy.js b/test/e2e-cypress/e2e/features/oas32/oas32-query-operation.cy.js
new file mode 100644
index 00000000000..6df62d3140c
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/oas32/oas32-query-operation.cy.js
@@ -0,0 +1,180 @@
+/**
+ * @prettier
+ */
+describe("OAS 3.2 QUERY Operation Support", () => {
+ const baseUrl = "/?url=/documents/features/oas32-query-operation.yaml"
+
+ describe("QUERY Operation Rendering", () => {
+ it("should render QUERY operation in the operations list", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .should("exist")
+ })
+
+ it("should display QUERY method with correct styling", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .should("have.class", "opblock-query")
+ })
+
+ it("should render QUERY operation summary", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .within(() => {
+ cy.get(".opblock-summary-description").should(
+ "contain.text",
+ "Search with complex query payload"
+ )
+ })
+ })
+
+ it("should display QUERY badge/label", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .within(() => {
+ cy.get(".opblock-summary-method").should("contain.text", "QUERY")
+ })
+ })
+ })
+
+ describe("QUERY Operation Expansion", () => {
+ it("should expand QUERY operation when clicked", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .should("have.class", "is-open")
+ })
+
+ it("should display operation description when expanded", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .within(() => {
+ cy.get(".opblock-description-wrapper").should("exist")
+ cy.get(".renderedMarkdown").should(
+ "contain.text",
+ "QUERY HTTP method"
+ )
+ })
+ })
+ })
+
+ describe("QUERY Operation Request Body", () => {
+ it("should render request body section", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .get(".opblock-section-request-body")
+ .should("exist")
+ })
+
+ it("should show request body is required", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .get(".opblock-section-request-body")
+ .should("exist")
+ .within(() => {
+ cy.get(".opblock-description-wrapper").should("exist")
+ })
+ })
+
+ it("should render request body schema with properties", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .get(".opblock-section-request-body")
+ .within(() => {
+ cy.contains("query").should("exist")
+ cy.contains("filters").should("exist")
+ cy.contains("pagination").should("exist")
+ })
+ })
+ })
+
+ describe("QUERY Operation Responses", () => {
+ it("should render response section", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .get(".responses-wrapper")
+ .should("exist")
+ })
+
+ it("should display 200 response", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .get(".responses-wrapper")
+ .within(() => {
+ cy.contains("200").should("exist")
+ cy.contains("Search results").should("exist")
+ })
+ })
+
+ it("should display error responses", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchWithQuery")
+ .click()
+ .get(".responses-wrapper")
+ .within(() => {
+ cy.contains("400").should("exist")
+ cy.contains("413").should("exist")
+ })
+ })
+ })
+
+ describe("Mixed Operations on Same Path", () => {
+ it("should render both GET and QUERY operations for /products/search", () => {
+ cy.visit(baseUrl)
+ cy.get("#operations-Search-searchProducts").should("exist")
+ cy.get("#operations-Search-advancedSearchProducts").should("exist")
+ })
+
+ it("should distinguish GET and QUERY operations visually", () => {
+ cy.visit(baseUrl)
+ cy.get("#operations-Search-searchProducts")
+ .should("have.class", "opblock-get")
+ .within(() => {
+ cy.get(".opblock-summary-method").should("contain.text", "GET")
+ })
+
+ cy.get("#operations-Search-advancedSearchProducts")
+ .should("have.class", "opblock-query")
+ .within(() => {
+ cy.get(".opblock-summary-method").should("contain.text", "QUERY")
+ })
+ })
+
+ it("should show GET operation with query parameters", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-searchProducts")
+ .click()
+ .within(() => {
+ cy.contains("Parameters").should("exist")
+ cy.contains("q").should("exist")
+ })
+ })
+
+ it("should show QUERY operation with request body", () => {
+ cy.visit(baseUrl)
+ .get("#operations-Search-advancedSearchProducts")
+ .click()
+ .get(".opblock-section-request-body")
+ .should("exist")
+ .within(() => {
+ cy.contains("priceRange").should("exist")
+ cy.contains("specifications").should("exist")
+ })
+ })
+ })
+
+ describe("OAS Version Detection", () => {
+ it("should display OAS 3.2 badge", () => {
+ cy.visit(baseUrl)
+ .get(".info .version-stamp")
+ .contains("OAS 3.2")
+ .should("exist")
+ })
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/oas32/oas32-version-detection.cy.js b/test/e2e-cypress/e2e/features/oas32/oas32-version-detection.cy.js
new file mode 100644
index 00000000000..e75f0aeaefa
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/oas32/oas32-version-detection.cy.js
@@ -0,0 +1,21 @@
+/**
+ * @prettier
+ */
+
+describe("OpenAPI 3.2.0 - Version Detection", () => {
+ const baseUrl = "/?url=/documents/oas32/oas32-features.yaml"
+
+ it("should detect and render OAS 3.2.0 spec with all info fields", () => {
+ cy.visit(baseUrl)
+ .get(".information-container")
+ .should("exist")
+ .find(".title")
+ .should("contain", "OAS 3.2.0 Basic Features")
+ .get(".information-container .description")
+ .should("contain", "basic features implemented for OpenAPI 3.2.0")
+ .get(".information-container .info__summary")
+ .should("contain", "Demonstrates basic OpenAPI 3.2.0 implementation")
+ .get(".version-pragma__message--missing")
+ .should("not.exist")
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/plugins/oas32/oas32-json-schema-rendering.cy.js b/test/e2e-cypress/e2e/features/plugins/oas32/oas32-json-schema-rendering.cy.js
new file mode 100644
index 00000000000..015a9732bf3
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/plugins/oas32/oas32-json-schema-rendering.cy.js
@@ -0,0 +1,76 @@
+/**
+ * @prettier
+ */
+
+describe("OpenAPI 3.2 JSON Schema 2020-12 rendering", () => {
+ const baseUrl = "/?url=/documents/features/oas32-json-schema-rendering.yaml"
+
+ describe("Schemas section", () => {
+ beforeEach(() => {
+ cy.visit(baseUrl)
+ })
+
+ it("should render the schemas section using JSON Schema 2020-12", () => {
+ cy.get(".json-schema-2020-12").should("exist")
+ })
+
+ it("should render schema properties with JSON Schema 2020-12 property classes", () => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ cy.get(".json-schema-2020-12-property").contains("id").should("exist")
+ cy.get(".json-schema-2020-12-property").contains("name").should("exist")
+ })
+
+ it("should render the schema description keyword", () => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ cy.get(".json-schema-2020-12-keyword--description")
+ .should("exist")
+ .and("contain.text", "A pet in the system")
+ })
+
+ it("should render the Discriminator keyword", () => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ cy.get(".json-schema-2020-12-keyword__name")
+ .contains("Discriminator")
+ .should("exist")
+ })
+
+ it("should render the External documentation keyword", () => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ cy.get(".json-schema-2020-12-keyword__name")
+ .contains("External documentation")
+ .should("exist")
+ })
+
+ it("should render the XML keyword", () => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ cy.get(".json-schema-2020-12-keyword--xml").should("exist")
+ })
+
+ it("should render the Examples keyword", () => {
+ cy.get(".json-schema-2020-12").contains("My Pet").click()
+ cy.get(".json-schema-2020-12-keyword--examples").should("exist")
+ })
+ })
+
+ describe("Request body schema", () => {
+ beforeEach(() => {
+ cy.visit(baseUrl)
+ cy.get(".opblock-summary-path span").contains("/pets").click()
+ cy.get("button").contains("Try it out").click()
+ })
+
+ it("should render request body schema using JSON Schema 2020-12", () => {
+ cy.get(".model-example button").contains("Schema").click()
+ cy.get(".model-example .json-schema-2020-12").should("exist")
+ })
+
+ it("should render example for properties with union type including object", () => {
+ cy.get(".model-example textarea")
+ .should("exist")
+ .and(
+ "have.value",
+ '{\n "objectTypeUnion": {\n "id": "string",\n "name": "string"\n }\n}'
+ )
+ })
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/plugins/oas32/oas32-request-body-complex-schema-properties.cy.js b/test/e2e-cypress/e2e/features/plugins/oas32/oas32-request-body-complex-schema-properties.cy.js
new file mode 100644
index 00000000000..7e15904ae4c
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/plugins/oas32/oas32-request-body-complex-schema-properties.cy.js
@@ -0,0 +1,89 @@
+/**
+ * @prettier
+ */
+
+describe("OpenAPI 3.2 request body properties with schema and union type", () => {
+ beforeEach(() => {
+ cy.visit(
+ "/?url=/documents/features/oas32-request-body-complex-schema-properties.yaml"
+ )
+ })
+
+ it("should render example for properties with union type including object", () => {
+ cy.get(".opblock-summary-path span").contains("/objectTypeUnion").click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example textarea")
+ .should("exist")
+ .and("have.value", '{\n "id": "string",\n "name": "string"\n}')
+ })
+
+ it("should render schema for properties with union type including object", () => {
+ cy.get(".opblock-summary-path span").contains("/objectTypeUnion").click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example button").contains("Schema").click()
+ cy.get(".model-example .json-schema-2020-12").should("exist")
+ })
+
+ it("should render example for properties with union type including array of objects", () => {
+ cy.get(".opblock-summary-path span").contains("/arrayTypeUnion").click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example textarea")
+ .should("exist")
+ .and(
+ "have.value",
+ '[\n {\n "id": "string",\n "name": "string"\n }\n]'
+ )
+ })
+
+ it("should render schema for properties with union type including array of objects", () => {
+ cy.get(".opblock-summary-path span").contains("/arrayTypeUnion").click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example button").contains("Schema").click()
+ cy.get(".model-example .json-schema-2020-12").should("exist")
+ })
+
+ it("should render example for properties of type array with union type of items including object", () => {
+ cy.get(".opblock-summary-path span").contains("/arrayItemTypeUnion").click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example textarea")
+ .should("exist")
+ .and("have.value", '{\n "id": "string",\n "name": "string"\n}')
+ })
+
+ it("should render schema for properties of type array with union type of items including object", () => {
+ cy.get(".opblock-summary-path span").contains("/arrayItemTypeUnion").click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example button").contains("Schema").click()
+ cy.get(".model-example .json-schema-2020-12").should("exist")
+ })
+
+ it("should render example for properties with union type including array and union type of items including object", () => {
+ cy.get(".opblock-summary-path span")
+ .contains("/arrayTypeAndItemTypeUnion")
+ .click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example textarea")
+ .should("exist")
+ .and(
+ "have.value",
+ '[\n {\n "id": "string",\n "name": "string"\n }\n]'
+ )
+ })
+
+ it("should render schema for properties with union type including array and union type of items including object", () => {
+ cy.get(".opblock-summary-path span")
+ .contains("/arrayTypeAndItemTypeUnion")
+ .click()
+ cy.get("button").contains("Try it out").click()
+
+ cy.get(".model-example button").contains("Schema").click()
+ cy.get(".model-example .json-schema-2020-12").should("exist")
+ })
+})
diff --git a/test/e2e-cypress/e2e/features/plugins/oas32/oas32-schema-expansion.cy.js b/test/e2e-cypress/e2e/features/plugins/oas32/oas32-schema-expansion.cy.js
new file mode 100644
index 00000000000..06f9845b307
--- /dev/null
+++ b/test/e2e-cypress/e2e/features/plugins/oas32/oas32-schema-expansion.cy.js
@@ -0,0 +1,49 @@
+/**
+ * @prettier
+ */
+
+describe("OpenAPI 3.2.0 schema expansion", () => {
+ it("should expand to the default expansion level", () => {
+ cy.visit(
+ "/?url=/documents/features/oas32-schema-expansion.yaml&defaultModelsExpandDepth=3&showExtensions=true"
+ )
+
+ cy.get(".json-schema-2020-12-property").contains("prop2").should("exist")
+ cy.get(".json-schema-2020-12-property")
+ .contains("prop3")
+ .should("not.exist")
+
+ cy.get(".json-schema-2020-12-keyword--xml")
+ .contains("x-extension")
+ .should("exist")
+ cy.get(".json-schema-2020-12-keyword--xml")
+ .contains("prop1")
+ .should("not.exist")
+ })
+
+ it("should deeply expand nested collapsed keywords", () => {
+ cy.visit(
+ "/?url=/documents/features/oas32-schema-expansion.yaml&showExtensions=true"
+ )
+
+ cy.get(".json-schema-2020-12-expand-deep-button").click()
+ cy.get(".json-schema-2020-12-keyword--xml")
+ .contains("prop4")
+ .should("exist")
+
+ cy.get(".json-schema-2020-12-keyword--xml").contains("prop1").click()
+ cy.get(".json-schema-2020-12-keyword--xml")
+ .contains("prop4")
+ .should("not.exist")
+
+ cy.get(".json-schema-2020-12-keyword--xml").contains("XML").click()
+ cy.get(
+ ".json-schema-2020-12-keyword--xml .json-schema-2020-12-expand-deep-button"
+ )
+ .first()
+ .click()
+ cy.get(".json-schema-2020-12-keyword--xml")
+ .contains("prop4")
+ .should("exist")
+ })
+})
diff --git a/test/e2e-cypress/static/documents/features/oas32-extension.yaml b/test/e2e-cypress/static/documents/features/oas32-extension.yaml
new file mode 100644
index 00000000000..c865ce3bf58
--- /dev/null
+++ b/test/e2e-cypress/static/documents/features/oas32-extension.yaml
@@ -0,0 +1,66 @@
+openapi: 3.2.0
+info:
+ title: Test
+ description: 'Test'
+ license:
+ name: 'Apache 2.0'
+ url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
+ version: '1.0'
+servers:
+ - url: http://petstore.swagger.io/v1
+components:
+ schemas:
+ Pet:
+ title: My Pet
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ tag:
+ type: string
+ discriminator:
+ propertyName: id
+ mapping:
+ x-primitiveExtension: 1
+ x-arrayExtension:
+ - 2
+ x-objectExtension:
+ prop: 3
+ default: default value
+ Object:
+ type: object
+ externalDocs:
+ description: Object Docs
+ url: http://swagger.io
+ x-primitiveExtension: 1
+ x-arrayExtension:
+ - 2
+ x-objectExtension:
+ prop: 3
+ default: default value
+ properties:
+ name:
+ type: string
+ Book:
+ type: object
+ properties:
+ id:
+ type: integer
+ title:
+ type: string
+ author:
+ type: string
+ xml:
+ prefix: "smp"
+ namespace: "http://example.com/schema"
+ x-primitiveExtension: 1
+ x-arrayExtension:
+ - 2
+ x-objectExtension:
+ prop: 3
diff --git a/test/e2e-cypress/static/documents/features/oas32-json-schema-rendering.yaml b/test/e2e-cypress/static/documents/features/oas32-json-schema-rendering.yaml
new file mode 100644
index 00000000000..623a57a5790
--- /dev/null
+++ b/test/e2e-cypress/static/documents/features/oas32-json-schema-rendering.yaml
@@ -0,0 +1,61 @@
+openapi: 3.2.0
+info:
+ title: Test
+ version: 1.0.0
+servers:
+ - url: http://example.com/v1
+paths:
+ /pets:
+ post:
+ operationId: createPet
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NewPet'
+ responses:
+ '201':
+ description: Created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+components:
+ schemas:
+ Pet:
+ title: My Pet
+ description: A pet in the system
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ tag:
+ type: string
+ readOnly: true
+ discriminator:
+ propertyName: id
+ externalDocs:
+ description: Find more info
+ url: http://example.com
+ xml:
+ name: Pet
+ prefix: pet
+ examples:
+ - id: 1
+ name: Fluffy
+ NewPet:
+ type: object
+ properties:
+ objectTypeUnion:
+ type: [object, integer]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
diff --git a/test/e2e-cypress/static/documents/features/oas32-query-operation.yaml b/test/e2e-cypress/static/documents/features/oas32-query-operation.yaml
new file mode 100644
index 00000000000..3270e0f364e
--- /dev/null
+++ b/test/e2e-cypress/static/documents/features/oas32-query-operation.yaml
@@ -0,0 +1,221 @@
+openapi: 3.2.0
+info:
+ title: OAS 3.2 QUERY Operation Test
+ version: 1.0.0
+ description: Test API demonstrating the QUERY HTTP method introduced in OAS 3.2
+
+paths:
+ /search:
+ query:
+ operationId: searchWithQuery
+ summary: Search with complex query payload
+ description: >
+ This endpoint demonstrates the QUERY HTTP method introduced in OAS 3.2.
+ The QUERY method is a new HTTP method in OAS 3.2 that allows sending
+ a request body with search parameters, providing more flexibility than GET.
+ tags:
+ - Search
+ requestBody:
+ description: Search payload with query, filters, and pagination
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ query:
+ type: string
+ description: Search query string
+ filters:
+ type: object
+ description: Additional filters to apply
+ properties:
+ category:
+ type: string
+ status:
+ type: string
+ pagination:
+ type: object
+ description: Pagination settings
+ properties:
+ page:
+ type: integer
+ minimum: 1
+ default: 1
+ pageSize:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ example:
+ query: "example search"
+ filters:
+ category: "electronics"
+ status: "active"
+ pagination:
+ page: 1
+ pageSize: 20
+ responses:
+ '200':
+ description: Search results
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ results:
+ type: array
+ items:
+ type: object
+ total:
+ type: integer
+ page:
+ type: integer
+ '400':
+ description: Invalid search criteria
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '413':
+ description: Payload too large
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /products/search:
+ get:
+ operationId: searchProducts
+ summary: Simple product search
+ description: Search products using query parameters (traditional GET)
+ tags:
+ - Search
+ parameters:
+ - name: q
+ in: query
+ description: Search query
+ schema:
+ type: string
+ - name: category
+ in: query
+ description: Product category filter
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Search results
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Product'
+
+ query:
+ operationId: advancedSearchProducts
+ summary: Advanced product search
+ description: >
+ Advanced product search using complex criteria in request body.
+ Demonstrates QUERY method alongside GET on the same path.
+ tags:
+ - Search
+ requestBody:
+ description: Advanced search criteria
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ priceRange:
+ type: object
+ properties:
+ min:
+ type: number
+ format: float
+ max:
+ type: number
+ format: float
+ specifications:
+ type: object
+ description: Product specifications to match
+ additionalProperties:
+ type: string
+ categories:
+ type: array
+ items:
+ type: string
+ sortBy:
+ type: string
+ enum: [price, rating, name, date]
+ example:
+ priceRange:
+ min: 10.0
+ max: 100.0
+ specifications:
+ brand: "Example Brand"
+ color: "blue"
+ categories: ["electronics", "computers"]
+ sortBy: "price"
+ responses:
+ '200':
+ description: Advanced search results
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/Product'
+ total:
+ type: integer
+ facets:
+ type: object
+ '400':
+ description: Invalid search criteria
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+components:
+ schemas:
+ Product:
+ type: object
+ required:
+ - id
+ - name
+ - price
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ price:
+ type: number
+ format: float
+ category:
+ type: string
+ specifications:
+ type: object
+ additionalProperties:
+ type: string
+ rating:
+ type: number
+ format: float
+ minimum: 0
+ maximum: 5
+
+ Error:
+ type: object
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ message:
+ type: string
diff --git a/test/e2e-cypress/static/documents/features/oas32-request-body-complex-schema-properties.yaml b/test/e2e-cypress/static/documents/features/oas32-request-body-complex-schema-properties.yaml
new file mode 100644
index 00000000000..6923d8193cb
--- /dev/null
+++ b/test/e2e-cypress/static/documents/features/oas32-request-body-complex-schema-properties.yaml
@@ -0,0 +1,83 @@
+openapi: 3.2.0
+info:
+ title: Request Body Example
+ version: 1.0.0
+paths:
+ /objectTypeUnion:
+ post:
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ objectTypeUnion:
+ type: [object, integer]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ responses:
+ '200':
+ description: OK
+ /arrayTypeUnion:
+ post:
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ arrayTypeUnion:
+ type: [array, integer]
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ responses:
+ '200':
+ description: OK
+ /arrayItemTypeUnion:
+ post:
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ arrayItemTypeUnion:
+ type: array
+ items:
+ type: [object, integer]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ responses:
+ '200':
+ description: OK
+ /arrayTypeAndItemTypeUnion:
+ post:
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ arrayTypeAndItemTypeUnion:
+ type: [array, integer]
+ items:
+ type: [object, string]
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ responses:
+ '200':
+ description: OK
diff --git a/test/e2e-cypress/static/documents/features/oas32-schema-expansion.yaml b/test/e2e-cypress/static/documents/features/oas32-schema-expansion.yaml
new file mode 100644
index 00000000000..ec43ec6d77d
--- /dev/null
+++ b/test/e2e-cypress/static/documents/features/oas32-schema-expansion.yaml
@@ -0,0 +1,19 @@
+openapi: 3.2.0
+components:
+ schemas:
+ Expansion:
+ properties:
+ prop1:
+ properties:
+ prop2:
+ properties:
+ prop3:
+ properties:
+ prop4:
+ type: string
+ xml:
+ x-extension:
+ prop1:
+ prop2:
+ prop3:
+ prop4: test
diff --git a/test/e2e-cypress/static/documents/oas32/component-only.yaml b/test/e2e-cypress/static/documents/oas32/component-only.yaml
new file mode 100644
index 00000000000..60ced11f8b9
--- /dev/null
+++ b/test/e2e-cypress/static/documents/oas32/component-only.yaml
@@ -0,0 +1,66 @@
+openapi: 3.2.0
+info:
+ title: Component-Only Specification
+ version: 1.0.0
+ description: |
+ This is a valid OAS 3.2.0 specification that contains only components,
+ without any paths or webhooks. This demonstrates the relaxed requirement
+ where at least one of components, paths, or webhooks must be present.
+
+components:
+ schemas:
+ User:
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ email:
+ type: string
+ format: email
+ created:
+ type: string
+ format: date-time
+
+ Error:
+ type: object
+ properties:
+ code:
+ type: integer
+ message:
+ type: string
+
+ responses:
+ NotFound:
+ description: Resource not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ Unauthorized:
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ parameters:
+ UserId:
+ name: userId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
diff --git a/test/e2e-cypress/static/documents/oas32/contact-and-license.yaml b/test/e2e-cypress/static/documents/oas32/contact-and-license.yaml
new file mode 100644
index 00000000000..76a2c5223db
--- /dev/null
+++ b/test/e2e-cypress/static/documents/oas32/contact-and-license.yaml
@@ -0,0 +1,31 @@
+openapi: 3.2.0
+info:
+ title: OAS 3.2.0 Contact and License Test
+ version: 1.0.0
+ description: This spec tests contact and license rendering in OAS 3.2.0
+ contact:
+ name: API Support Team
+ url: https://www.example.com/support
+ email: support@example.com
+ license:
+ name: Apache 2.0
+ url: https://www.apache.org/licenses/LICENSE-2.0.html
+
+servers:
+ - url: https://api.example.com/v1
+
+paths:
+ /test:
+ get:
+ summary: Test endpoint
+ operationId: test
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
diff --git a/test/e2e-cypress/static/documents/oas32/oas32-features.yaml b/test/e2e-cypress/static/documents/oas32/oas32-features.yaml
new file mode 100644
index 00000000000..f5130803e91
--- /dev/null
+++ b/test/e2e-cypress/static/documents/oas32/oas32-features.yaml
@@ -0,0 +1,61 @@
+openapi: 3.2.0
+info:
+ title: OAS 3.2.0 Basic Features
+ summary: Demonstrates basic OpenAPI 3.2.0 implementation
+ version: 1.0.0
+ description: |
+ This specification demonstrates the basic features implemented for OpenAPI 3.2.0:
+ - QUERY HTTP method
+ - Info summary field
+
+servers:
+ - url: https://api.example.com/v1
+
+paths:
+ /search:
+ query:
+ summary: Search with query payload
+ description: |
+ The QUERY method is a new HTTP method in OAS 3.2 that allows
+ safe, idempotent queries with a request body.
+ operationId: searchQuery
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ query:
+ type: string
+ example: "user:john"
+ filters:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [active, inactive]
+ responses:
+ '200':
+ description: Search results
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/SearchResult'
+
+components:
+ schemas:
+ SearchResult:
+ type: object
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+ score:
+ type: number
diff --git a/test/unit/core/plugins/oas32/components/version-pragma-filter.jsx b/test/unit/core/plugins/oas32/components/version-pragma-filter.jsx
new file mode 100644
index 00000000000..0acc4227d5b
--- /dev/null
+++ b/test/unit/core/plugins/oas32/components/version-pragma-filter.jsx
@@ -0,0 +1,115 @@
+/**
+ * @prettier
+ */
+import React from "react"
+import { shallow } from "enzyme"
+import VersionPragmaFilter from "core/plugins/oas32/components/version-pragma-filter"
+
+describe("OAS32 VersionPragmaFilter", () => {
+ it("should render children when version is valid (OAS 3.2)", () => {
+ const wrapper = shallow(
+
+ Test Content
+
+ )
+
+ expect(wrapper.find(".test-child")).toHaveLength(1)
+ expect(wrapper.text()).toContain("Test Content")
+ })
+
+ it("should render children when bypass is true", () => {
+ const wrapper = shallow(
+
+ Test Content
+
+ )
+
+ expect(wrapper.find(".test-child")).toHaveLength(1)
+ })
+
+ it("should render error when version is ambiguous (Swagger2 and OAS32)", () => {
+ const wrapper = shallow(
+
+ Test Content
+
+ )
+
+ expect(wrapper.find(".version-pragma__message--ambiguous")).toHaveLength(1)
+ expect(wrapper.text()).toContain("Unable to render this definition")
+ expect(wrapper.find(".test-child")).toHaveLength(0)
+ })
+
+ it("should render error when version is missing", () => {
+ const wrapper = shallow(
+
+ Test Content
+
+ )
+
+ expect(wrapper.find(".version-pragma__message--missing")).toHaveLength(1)
+ expect(wrapper.text()).toContain("Unable to render this definition")
+ expect(wrapper.find(".test-child")).toHaveLength(0)
+ })
+
+ it("should render children when only OAS3 is present", () => {
+ const wrapper = shallow(
+
+ Test Content
+
+ )
+
+ // Should render children (valid single version)
+ expect(wrapper.find(".test-child")).toHaveLength(1)
+ expect(wrapper.text()).toContain("Test Content")
+ })
+
+ it("should render alsoShow when provided and version is invalid", () => {
+ const alsoShowElement = Additional Info
+
+ const wrapper = shallow(
+
+ Test Content
+
+ )
+
+ expect(wrapper.find(".also-show")).toHaveLength(1)
+ expect(wrapper.text()).toContain("Additional Info")
+ })
+})
diff --git a/test/unit/core/plugins/oas32/fn.js b/test/unit/core/plugins/oas32/fn.js
new file mode 100644
index 00000000000..660d2c7ee21
--- /dev/null
+++ b/test/unit/core/plugins/oas32/fn.js
@@ -0,0 +1,62 @@
+/**
+ * @prettier
+ */
+import { fromJS } from "immutable"
+import { isOAS32 } from "core/plugins/oas32/fn"
+
+describe("oas32 plugin - fn - isOAS32", () => {
+ it("should match OpenAPI 3.2.0", () => {
+ const spec = fromJS({ openapi: "3.2.0" })
+ expect(isOAS32(spec)).toBe(true)
+ })
+
+ it("should match OpenAPI 3.2.1", () => {
+ const spec = fromJS({ openapi: "3.2.1" })
+ expect(isOAS32(spec)).toBe(true)
+ })
+
+ it("should match OpenAPI 3.2.25", () => {
+ const spec = fromJS({ openapi: "3.2.25" })
+ expect(isOAS32(spec)).toBe(true)
+ })
+
+ it("should NOT match OpenAPI 3.2", () => {
+ const spec = fromJS({ openapi: "3.2" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should NOT match OpenAPI 3.2.01 (leading zero)", () => {
+ const spec = fromJS({ openapi: "3.2.01" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should NOT match OpenAPI 3.1.0", () => {
+ const spec = fromJS({ openapi: "3.1.0" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should NOT match OpenAPI 3.0.3", () => {
+ const spec = fromJS({ openapi: "3.0.3" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should NOT match swagger: 2.0", () => {
+ const spec = fromJS({ swagger: "2.0" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should handle null spec", () => {
+ const spec = fromJS({})
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should NOT match OpenAPI 3.3.0", () => {
+ const spec = fromJS({ openapi: "3.3.0" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+
+ it("should NOT match OpenAPI 4.0.0", () => {
+ const spec = fromJS({ openapi: "4.0.0" })
+ expect(isOAS32(spec)).toBe(false)
+ })
+})
diff --git a/test/unit/core/plugins/oas32/query-operation-rendering.test.js b/test/unit/core/plugins/oas32/query-operation-rendering.test.js
new file mode 100644
index 00000000000..e11c6539acc
--- /dev/null
+++ b/test/unit/core/plugins/oas32/query-operation-rendering.test.js
@@ -0,0 +1,125 @@
+/**
+ * @prettier
+ */
+import { Map, List } from "immutable"
+import { validOperationMethods as validOperationMethodsWrapper } from "core/plugins/oas32/spec-extensions/wrap-selectors"
+import { validOperationMethods as oas32ValidOperationMethods } from "core/plugins/oas32/selectors"
+
+describe("OAS 3.2 QUERY operation rendering", () => {
+ describe("validOperationMethods wrapper", () => {
+ it("should include 'query' method for OAS 3.2 specs", () => {
+ const originalSelector = jest.fn(() => [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ ])
+
+ const system = {
+ getSystem: jest.fn(() => ({
+ specSelectors: {
+ isOAS32: jest.fn(() => true),
+ },
+ })),
+ oas32Selectors: {
+ validOperationMethods: oas32ValidOperationMethods,
+ },
+ }
+
+ const wrappedSelector = validOperationMethodsWrapper(
+ originalSelector,
+ system
+ )
+ const state = Map()
+ const result = wrappedSelector(state)
+
+ expect(result).toContain("query")
+ expect(result).toContain("get")
+ expect(result).toContain("post")
+ expect(result.length).toBe(9) // 8 standard + query
+ })
+
+ it("should not include 'query' for non-OAS32 specs", () => {
+ const originalSelector = jest.fn(() => [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ ])
+
+ const system = {
+ getSystem: jest.fn(() => ({
+ specSelectors: {
+ isOAS32: jest.fn(() => false),
+ },
+ })),
+ oas32Selectors: {
+ validOperationMethods: oas32ValidOperationMethods,
+ },
+ }
+
+ const wrappedSelector = validOperationMethodsWrapper(
+ originalSelector,
+ system
+ )
+ const state = Map()
+ const result = wrappedSelector(state)
+
+ expect(result).not.toContain("query")
+ expect(result.length).toBe(8)
+ })
+ })
+
+ describe("integration test", () => {
+ it("should allow Operations component to render QUERY operations", () => {
+ // Simulate what the Operations component does
+ const validOperationMethods = [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ "query",
+ ]
+
+ const operations = List([
+ Map({
+ path: "/pets",
+ method: "get",
+ operation: Map({ summary: "Get pets" }),
+ }),
+ Map({
+ path: "/pets",
+ method: "query",
+ operation: Map({ summary: "Search pets" }),
+ }),
+ Map({
+ path: "/pets",
+ method: "post",
+ operation: Map({ summary: "Create pet" }),
+ }),
+ ])
+
+ // Filter operations like Operations component does
+ const renderedOperations = operations.filter(
+ (op) => validOperationMethods.indexOf(op.get("method")) !== -1
+ )
+
+ expect(renderedOperations.size).toBe(3)
+ expect(
+ renderedOperations.find((op) => op.get("method") === "query")
+ ).toBeDefined()
+ })
+ })
+})
diff --git a/test/unit/core/plugins/oas32/selectors.test.js b/test/unit/core/plugins/oas32/selectors.test.js
new file mode 100644
index 00000000000..a5c1eeb5410
--- /dev/null
+++ b/test/unit/core/plugins/oas32/selectors.test.js
@@ -0,0 +1,37 @@
+/**
+ * @prettier
+ */
+import { validOperationMethods } from "core/plugins/oas32/selectors"
+
+describe("oas32 plugin - selectors", () => {
+ describe("validOperationMethods", () => {
+ it("should return an array of valid operation methods", () => {
+ const result = validOperationMethods()
+ expect(Array.isArray(result)).toBe(true)
+ expect(result.length).toBe(9)
+ })
+
+ it("should include standard HTTP methods", () => {
+ const result = validOperationMethods()
+ expect(result).toContain("get")
+ expect(result).toContain("put")
+ expect(result).toContain("post")
+ expect(result).toContain("delete")
+ expect(result).toContain("options")
+ expect(result).toContain("head")
+ expect(result).toContain("patch")
+ expect(result).toContain("trace")
+ })
+
+ it("should include QUERY method for OAS 3.2", () => {
+ const result = validOperationMethods()
+ expect(result).toContain("query")
+ })
+
+ it("should return the same array reference on multiple calls", () => {
+ const result1 = validOperationMethods()
+ const result2 = validOperationMethods()
+ expect(result1).toBe(result2)
+ })
+ })
+})
diff --git a/test/unit/core/plugins/oas32/spec-extensions/wrap-selectors.test.js b/test/unit/core/plugins/oas32/spec-extensions/wrap-selectors.test.js
new file mode 100644
index 00000000000..6f336cabcdb
--- /dev/null
+++ b/test/unit/core/plugins/oas32/spec-extensions/wrap-selectors.test.js
@@ -0,0 +1,85 @@
+/**
+ * @prettier
+ */
+import { Map } from "immutable"
+import { validOperationMethods as validOperationMethodsWrapper } from "core/plugins/oas32/spec-extensions/wrap-selectors"
+
+describe("OAS32 wrap-selectors", () => {
+ describe("validOperationMethods", () => {
+ it("should include query for OAS 3.2 specs", () => {
+ const oriSelector = jest.fn(() => [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ ])
+
+ const oas32Methods = [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ "query",
+ ]
+
+ const system = {
+ getSystem: jest.fn(() => ({
+ specSelectors: {
+ isOAS32: jest.fn(() => true),
+ },
+ })),
+ oas32Selectors: {
+ validOperationMethods: jest.fn(() => oas32Methods),
+ },
+ }
+
+ const wrappedSelector = validOperationMethodsWrapper(oriSelector, system)
+ const state = Map()
+ const result = wrappedSelector(state)
+
+ expect(result).toContain("query")
+ expect(system.oas32Selectors.validOperationMethods).toHaveBeenCalled()
+ })
+
+ it("should not include query for non-OAS32 specs", () => {
+ const oas3Methods = [
+ "get",
+ "put",
+ "post",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+ ]
+ const oriSelector = jest.fn(() => oas3Methods)
+
+ const system = {
+ getSystem: jest.fn(() => ({
+ specSelectors: {
+ isOAS32: jest.fn(() => false),
+ },
+ })),
+ oas32Selectors: {
+ validOperationMethods: jest.fn(),
+ },
+ }
+
+ const wrappedSelector = validOperationMethodsWrapper(oriSelector, system)
+ const state = Map()
+ const result = wrappedSelector(state)
+
+ expect(result).not.toContain("query")
+ expect(oriSelector).toHaveBeenCalled()
+ expect(system.oas32Selectors.validOperationMethods).not.toHaveBeenCalled()
+ })
+ })
+})