From 9f2339234630e728ef21daf448048dae6c481c6e Mon Sep 17 00:00:00 2001 From: adrianodpdiaz Date: Thu, 2 Apr 2026 16:25:33 +0200 Subject: [PATCH 1/4] feat(oas31/style): array items label schema name (#10794) --- .../components/keywords/Items.jsx | 3 ++- src/core/plugins/oas31/after-load.js | 8 ++++++- .../json-schema-2020-12-extensions/fn.js | 22 +++++++++++++++++++ .../plugins/oas31/wrap-components/model.jsx | 1 + .../plugins/oas31/wrap-components/models.jsx | 1 + 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx b/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx index 95f6523459a..a5b575528b5 100644 --- a/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx +++ b/src/core/plugins/json-schema-2020-12/components/keywords/Items.jsx @@ -15,9 +15,10 @@ const Items = ({ schema }) => { */ if (!fn.hasKeyword(schema, "items")) return null + const itemsTitle = fn.getTitle(schema.items) const name = ( - Items + {itemsTitle || "Items"} ) diff --git a/src/core/plugins/oas31/after-load.js b/src/core/plugins/oas31/after-load.js index 7e6be340760..77fc44062ae 100644 --- a/src/core/plugins/oas31/after-load.js +++ b/src/core/plugins/oas31/after-load.js @@ -3,6 +3,7 @@ */ import { makeIsExpandable, + makeGetTitle, getProperties, } from "./json-schema-2020-12-extensions/fn" import { wrapOAS31Fn } from "./fn" @@ -16,8 +17,13 @@ function afterLoad({ fn, getSystem }) { fn.jsonSchema202012.isExpandable, getSystem ) + const getTitle = makeGetTitle(fn.jsonSchema202012.getTitle) - Object.assign(this.fn.jsonSchema202012, { isExpandable, getProperties }) + Object.assign(this.fn.jsonSchema202012, { + isExpandable, + getProperties, + ...(getTitle && { getTitle }), + }) } // wraps schema generators from samples plugin and make them specific to OpenAPI 3.1 version diff --git a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js index bb158801ecd..1205b797c50 100644 --- a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js +++ b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -1,6 +1,28 @@ /** * @prettier */ +export const makeGetTitle = (original) => { + if (typeof original !== "function") { + return null + } + + return (schema, options) => { + const title = original(schema, options) + if (title) return title + if (typeof schema?.$$ref === "string") { + const match = schema.$$ref.match(/#\/components\/schemas\/([^/]+)$/) + if (match) { + try { + return decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~")) + } catch { + return match[1] + } + } + } + return "" + } +} + export const makeIsExpandable = (original, getSystem) => { const { fn } = getSystem() diff --git a/src/core/plugins/oas31/wrap-components/model.jsx b/src/core/plugins/oas31/wrap-components/model.jsx index 3a7e30ae77d..c5336ed0e87 100644 --- a/src/core/plugins/oas31/wrap-components/model.jsx +++ b/src/core/plugins/oas31/wrap-components/model.jsx @@ -39,6 +39,7 @@ const ModelWrapper = createOnlyOAS31ComponentWrapper( fn: { getProperties: fn.jsonSchema202012.getProperties, isExpandable: fn.jsonSchema202012.isExpandable, + getTitle: fn.jsonSchema202012.getTitle, getSchemaKeywords: makeGetSchemaKeywords( fn.jsonSchema202012.getSchemaKeywords ), diff --git a/src/core/plugins/oas31/wrap-components/models.jsx b/src/core/plugins/oas31/wrap-components/models.jsx index 0c1fd763885..3a00921477b 100644 --- a/src/core/plugins/oas31/wrap-components/models.jsx +++ b/src/core/plugins/oas31/wrap-components/models.jsx @@ -37,6 +37,7 @@ const ModelsWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => { fn: { getProperties: fn.jsonSchema202012.getProperties, isExpandable: fn.jsonSchema202012.isExpandable, + getTitle: fn.jsonSchema202012.getTitle, getSchemaKeywords: makeGetSchemaKeywords( fn.jsonSchema202012.getSchemaKeywords ), From 6f2fadd8cb6428e4ea2ad30ffda8daed4ab151ca Mon Sep 17 00:00:00 2001 From: adrianodpdiaz Date: Thu, 2 Apr 2026 17:17:26 +0200 Subject: [PATCH 2/4] add unit and e2e tests --- .../features/oas31-array-items-label.cy.js | 25 ++++++++ .../features/oas31-array-items-label.yaml | 35 +++++++++++ .../json-schema-2020-12-extensions/fn.js | 59 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js create mode 100644 test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml create mode 100644 test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js diff --git a/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js b/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js new file mode 100644 index 00000000000..462e9aed6a1 --- /dev/null +++ b/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js @@ -0,0 +1,25 @@ +/** + * @prettier + */ +describe("OpenAPI 3.1 array items label", () => { + beforeEach(() => { + cy.visit("/?url=/documents/features/oas31-array-items-label.yaml") + cy.get(".models").click() + cy.get(".json-schema-2020-12").contains("UserList").click() + cy.get(".json-schema-2020-12-accordion") + .contains("users") + .click() + }) + + it("should display the referenced schema name instead of 'Items' for array items", () => { + cy.get(".json-schema-2020-12-keyword--items") + .find(".json-schema-2020-12-keyword__name--primary") + .should("have.text", "User") + }) + + it("should not display the generic 'Items' label when items is a $ref", () => { + cy.get(".json-schema-2020-12-keyword--items") + .find(".json-schema-2020-12-keyword__name--primary") + .should("not.have.text", "Items") + }) +}) diff --git a/test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml b/test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml new file mode 100644 index 00000000000..5608fdb17d2 --- /dev/null +++ b/test/e2e-cypress/static/documents/features/oas31-array-items-label.yaml @@ -0,0 +1,35 @@ +openapi: 3.1.0 +info: + title: Test + version: "1.0" +paths: + /users: + get: + summary: Get users + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UserList" +components: + schemas: + User: + type: object + description: A user of the system. + properties: + id: + type: string + name: + type: string + UserList: + type: object + description: A list of users. + properties: + results: + type: integer + users: + type: array + items: + $ref: "#/components/schemas/User" diff --git a/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js new file mode 100644 index 00000000000..7ed11d3776a --- /dev/null +++ b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -0,0 +1,59 @@ +/** + * @prettier + */ +import { makeGetTitle } from "core/plugins/oas31/json-schema-2020-12-extensions/fn" + +const baseGetTitle = (schema) => schema?.title ?? "" + +describe("OAS31 - json-schema-2020-12-extensions - makeGetTitle", () => { + const getTitle = makeGetTitle(baseGetTitle) + + it("should return null when original is not a function", () => { + expect(makeGetTitle(null)).toBeNull() + expect(makeGetTitle(undefined)).toBeNull() + expect(makeGetTitle("string")).toBeNull() + }) + + describe("when schema has a title", () => { + it("should return the title from the original getTitle", () => { + expect(getTitle({ title: "MyTitle" })).toBe("MyTitle") + }) + + it("should prefer title over $$ref", () => { + expect( + getTitle({ + title: "MyTitle", + $$ref: "#/components/schemas/Foo", + }) + ).toBe("MyTitle") + }) + }) + + describe("when schema has no title but has $$ref", () => { + it("should extract the schema name from a full URL $$ref", () => { + expect( + getTitle({ + $$ref: "http://localhost:3200/swagger.json#/components/schemas/Foo", + }) + ).toBe("Foo") + }) + + it("should extract the schema name from a relative $$ref", () => { + expect(getTitle({ $$ref: "swagger.json#/components/schemas/Bar" })).toBe("Bar") + }) + + it("should extract the schema name from a fragment-only $$ref", () => { + expect(getTitle({ $$ref: "#/components/schemas/Foo" })).toBe("Foo") + }) + }) + + describe("when schema has no title and $$ref does not point to components/schemas", () => { + it("should return empty string for a non-schema $$ref", () => { + expect(getTitle({ $$ref: "#/definitions/Bar" })).toBe("") + }) + + it("should return empty string for null schema", () => { + expect(getTitle(null)).toBe("") + }) + }) +}) From fd6f4d64acdd4084257b8c09c6c392cd8c92ab9a Mon Sep 17 00:00:00 2001 From: adrianodpdiaz Date: Wed, 8 Apr 2026 18:17:19 +0200 Subject: [PATCH 3/4] prettier fixes --- src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js | 4 +++- test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js | 4 +--- .../core/plugins/oas31/json-schema-2020-12-extensions/fn.js | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js index 1205b797c50..a69b3f64053 100644 --- a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js +++ b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -13,7 +13,9 @@ export const makeGetTitle = (original) => { const match = schema.$$ref.match(/#\/components\/schemas\/([^/]+)$/) if (match) { try { - return decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~")) + return decodeURIComponent( + match[1].replace(/~1/g, "/").replace(/~0/g, "~") + ) } catch { return match[1] } diff --git a/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js b/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js index 462e9aed6a1..97dad2c0bc4 100644 --- a/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js +++ b/test/e2e-cypress/e2e/features/oas31-array-items-label.cy.js @@ -6,9 +6,7 @@ describe("OpenAPI 3.1 array items label", () => { cy.visit("/?url=/documents/features/oas31-array-items-label.yaml") cy.get(".models").click() cy.get(".json-schema-2020-12").contains("UserList").click() - cy.get(".json-schema-2020-12-accordion") - .contains("users") - .click() + cy.get(".json-schema-2020-12-accordion").contains("users").click() }) it("should display the referenced schema name instead of 'Items' for array items", () => { diff --git a/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js index 7ed11d3776a..d3c06230799 100644 --- a/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js +++ b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -39,7 +39,9 @@ describe("OAS31 - json-schema-2020-12-extensions - makeGetTitle", () => { }) it("should extract the schema name from a relative $$ref", () => { - expect(getTitle({ $$ref: "swagger.json#/components/schemas/Bar" })).toBe("Bar") + expect(getTitle({ $$ref: "swagger.json#/components/schemas/Bar" })).toBe( + "Bar" + ) }) it("should extract the schema name from a fragment-only $$ref", () => { From 264f3907953ef4bba52cfe93828905b9caf2201d Mon Sep 17 00:00:00 2001 From: adrianodpdiaz Date: Thu, 16 Apr 2026 14:28:07 +0200 Subject: [PATCH 4/4] reuse existing getModelName --- .../json-schema-5/components/model.jsx | 24 +++---------------- .../plugins/oas31/components/model/model.jsx | 15 +----------- .../json-schema-2020-12-extensions/fn.js | 16 +++---------- src/core/utils/get-model-name.js | 24 +++++++++++++++++++ test/unit/core/helpers/get-model-name.js | 12 ++++------ .../json-schema-2020-12-extensions/fn.js | 12 +++++++--- 6 files changed, 45 insertions(+), 58 deletions(-) create mode 100644 src/core/utils/get-model-name.js diff --git a/src/core/plugins/json-schema-5/components/model.jsx b/src/core/plugins/json-schema-5/components/model.jsx index 1fb70d386bf..690d1b47d68 100644 --- a/src/core/plugins/json-schema-5/components/model.jsx +++ b/src/core/plugins/json-schema-5/components/model.jsx @@ -5,16 +5,7 @@ import PropTypes from "prop-types" import { Map } from "immutable" import RollingLoadSVG from "core/assets/rolling-load.svg" - -const decodeRefName = uri => { - const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~") - - try { - return decodeURIComponent(unescaped) - } catch { - return unescaped - } -} +import { getModelName } from "core/utils/get-model-name" export default class Model extends ImmutablePureComponent { static propTypes = { @@ -33,15 +24,6 @@ export default class Model extends ImmutablePureComponent { includeWriteOnly: PropTypes.bool, } - getModelName =( ref )=> { - if ( ref.indexOf("#/definitions/") !== -1 ) { - return decodeRefName(ref.replace(/^.*#\/definitions\//, "")) - } - if ( ref.indexOf("#/components/schemas/") !== -1 ) { - return decodeRefName(ref.replace(/^.*#\/components\/schemas\//, "")) - } - } - getRefSchema =( model )=> { let { specSelectors } = this.props @@ -60,7 +42,7 @@ export default class Model extends ImmutablePureComponent { // If we weren't passed a `name` but have a resolved ref, grab the name from the ref if (!name && $$ref) { - name = this.getModelName($$ref) + name = getModelName($$ref) } /* @@ -71,7 +53,7 @@ export default class Model extends ImmutablePureComponent { * - we had a circular ref inside the allOf keyword */ if ($ref) { - const refName = this.getModelName($ref) + const refName = getModelName($ref) const refSchema = this.getRefSchema(refName) if (Map.isMap(refSchema)) { schema = refSchema.mergeDeep(schema) diff --git a/src/core/plugins/oas31/components/model/model.jsx b/src/core/plugins/oas31/components/model/model.jsx index 33ae7dbb4ce..d3a75488fc3 100644 --- a/src/core/plugins/oas31/components/model/model.jsx +++ b/src/core/plugins/oas31/components/model/model.jsx @@ -5,20 +5,7 @@ import React, { forwardRef, useCallback } from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" -const decodeRefName = (uri) => { - const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~") - try { - return decodeURIComponent(unescaped) - } catch { - return unescaped - } -} -const getModelName = (uri) => { - if (typeof uri === "string" && uri.includes("#/components/schemas/")) { - return decodeRefName(uri.replace(/^.*#\/components\/schemas\//, "")) - } - return null -} +import { getModelName } from "../../../../utils/get-model-name" const Model = forwardRef( ({ schema, getComponent, onToggle = () => {}, specPath }, ref) => { diff --git a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js index a69b3f64053..0d62df1db25 100644 --- a/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js +++ b/src/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -1,6 +1,8 @@ /** * @prettier */ +import { getModelName } from "../../../utils/get-model-name" + export const makeGetTitle = (original) => { if (typeof original !== "function") { return null @@ -9,19 +11,7 @@ export const makeGetTitle = (original) => { return (schema, options) => { const title = original(schema, options) if (title) return title - if (typeof schema?.$$ref === "string") { - const match = schema.$$ref.match(/#\/components\/schemas\/([^/]+)$/) - if (match) { - try { - return decodeURIComponent( - match[1].replace(/~1/g, "/").replace(/~0/g, "~") - ) - } catch { - return match[1] - } - } - } - return "" + return getModelName(schema?.$$ref) || "" } } diff --git a/src/core/utils/get-model-name.js b/src/core/utils/get-model-name.js new file mode 100644 index 00000000000..450d5fd5dae --- /dev/null +++ b/src/core/utils/get-model-name.js @@ -0,0 +1,24 @@ +/** + * @prettier + */ +export const decodeRefName = (uri) => { + const unescaped = uri.replace(/~1/g, "/").replace(/~0/g, "~") + + try { + return decodeURIComponent(unescaped) + } catch { + return unescaped + } +} + +export const getModelName = (ref) => { + if (typeof ref !== "string") return null + + if (ref.indexOf("#/definitions/") !== -1) { + return decodeRefName(ref.replace(/^.*#\/definitions\//, "")) + } + if (ref.indexOf("#/components/schemas/") !== -1) { + return decodeRefName(ref.replace(/^.*#\/components\/schemas\//, "")) + } + return null +} diff --git a/test/unit/core/helpers/get-model-name.js b/test/unit/core/helpers/get-model-name.js index 63407055702..555ac300ad6 100644 --- a/test/unit/core/helpers/get-model-name.js +++ b/test/unit/core/helpers/get-model-name.js @@ -2,20 +2,18 @@ * @prettier */ -import Model from "../../../../src/core/plugins/json-schema-5/components/model" +import { getModelName } from "../../../../src/core/utils/get-model-name" describe("getModelName", () => { - const model = new Model() - it("should decode JSON Pointer and URI encoding for OpenAPI v3 refs", () => { - const actual = model.getModelName("#/components/schemas/a~1b%2Bc") + const actual = getModelName("#/components/schemas/a~1b%2Bc") const expected = "a/b+c" expect(actual).toStrictEqual(expected) }) it("should decode JSON Pointer and URI encoding for Swagger v2 refs", () => { - const actual = model.getModelName( + const actual = getModelName( "#/definitions/custom%3A%3Anamespace%3A%3APerson" ) const expected = "custom::namespace::Person" @@ -24,14 +22,14 @@ describe("getModelName", () => { }) it("should decode multiple json-pointer values", () => { - const actual = model.getModelName("#/components/schemas/~1~1~0~0") + const actual = getModelName("#/components/schemas/~1~1~0~0") const expected = "//~~" expect(actual).toStrictEqual(expected) }) it("should support invalid URI encoding", () => { - const actual = model.getModelName("#/components/schemas/%25%d") + const actual = getModelName("#/components/schemas/%25%d") const expected = "%25%d" expect(actual).toStrictEqual(expected) diff --git a/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js index d3c06230799..b5433264d09 100644 --- a/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js +++ b/test/unit/core/plugins/oas31/json-schema-2020-12-extensions/fn.js @@ -49,9 +49,15 @@ describe("OAS31 - json-schema-2020-12-extensions - makeGetTitle", () => { }) }) - describe("when schema has no title and $$ref does not point to components/schemas", () => { - it("should return empty string for a non-schema $$ref", () => { - expect(getTitle({ $$ref: "#/definitions/Bar" })).toBe("") + describe("when schema has no title but has $$ref pointing to #/definitions/", () => { + it("should extract the schema name from a definitions $$ref", () => { + expect(getTitle({ $$ref: "#/definitions/Bar" })).toBe("Bar") + }) + }) + + describe("when schema has no title and $$ref does not match known paths", () => { + it("should return empty string for an unrecognized $$ref", () => { + expect(getTitle({ $$ref: "#/other/path/Baz" })).toBe("") }) it("should return empty string for null schema", () => {