From 902f211bca84f88bf002aed936c84d9bc81ac17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Roucheray?= Date: Fri, 30 Oct 2026 12:48:50 +0100 Subject: [PATCH 1/2] fix(rapidoc): untyped schemas with only annotations are shown as any getTypeInfo only returned "any" for a completely empty schema. As soon as an untyped schema carried a description (e.g. an `items` documented as "A string, a number or a boolean"), the schema table rendered {missing-type-info}. A schema that only carries annotations (title, description, example(s), default, deprecated, readOnly, writeOnly, externalDocs, x-*) is now "any". A schema with constraints but no type (e.g. minLength) still reports a missing type. Co-Authored-By: Claude Sonnet 5.5 --- packages/rapidoc/src/utils/schema-utils.js | 19 ++++++++++++++++++- tests/unit/schema-utils.test.js | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/rapidoc/src/utils/schema-utils.js b/packages/rapidoc/src/utils/schema-utils.js index 60c6238c..a68380fd 100644 --- a/packages/rapidoc/src/utils/schema-utils.js +++ b/packages/rapidoc/src/utils/schema-utils.js @@ -129,6 +129,23 @@ export function isBinaryFileField(schema) { ); } +// Keywords that only annotate a schema, without telling anything about the type of the value +const ANNOTATION_KEYWORDS = new Set([ + "title", + "description", + "example", + "examples", + "default", + "deprecated", + "readOnly", + "writeOnly", + "externalDocs", +]); + +function isAnnotationOnly(schema) { + return Object.keys(schema).every((key) => ANNOTATION_KEYWORDS.has(key) || key.startsWith("x-")); +} + /* Generates an schema object containing type and constraint info */ export function getTypeInfo(schema) { if (!schema) { @@ -170,7 +187,7 @@ export function getTypeInfo(schema) { } }); dataType = subTypes.length > 0 ? subTypes.join("┃") : "{missing-type-info}"; - } else if (Object.keys(schema).length === 0) { + } else if (isAnnotationOnly(schema)) { dataType = "any"; } else { dataType = "{missing-type-info}"; diff --git a/tests/unit/schema-utils.test.js b/tests/unit/schema-utils.test.js index f62c830a..caa15b82 100644 --- a/tests/unit/schema-utils.test.js +++ b/tests/unit/schema-utils.test.js @@ -327,6 +327,19 @@ describe("generateExample", () => { }); describe("getTypeInfo", () => { + it("should return any for a schema that only carries annotations", () => { + assert.strictEqual(getTypeInfo({}).type, "any"); + assert.strictEqual(getTypeInfo({ description: "A string, a number or a boolean" }).type, "any"); + assert.strictEqual( + getTypeInfo({ title: "Value", description: "Anything", example: 1, "x-internal": true }).type, + "any", + ); + }); + + it("should still report a missing type when the schema has constraints but no type", () => { + assert.strictEqual(getTypeInfo({ minLength: 3 }).type, "{missing-type-info}"); + }); + it("should format non-string const values as strings in allowedValues", () => { const numSchema = { type: "number", const: 1 }; const info = getTypeInfo(numSchema); From ecac3f8d3f85486638135649f08ac14afe27876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Roucheray?= Date: Fri, 30 Oct 2026 13:11:13 +0100 Subject: [PATCH 2/2] fix(rapidoc): show the primitive options of a oneOf on a single row A primitive option (string, number, boolean...) of a oneOf/anyOf took two rows: the OPT n badge, then a row named "Option n" repeating it before the type. The badge (and the title if any) now sits in the key cell of the primitive row, next to its type and description. Titled options and untitled objects/arrays keep their header row, it carries the description and hosts the children. Co-Authored-By: Claude Sonnet 5.5 --- .../rapidoc/src/components/schema-table.ts | 52 ++++++++++++++----- packages/rapidoc/src/utils/schema-utils.js | 4 ++ tests/unit/schema-ast.test.js | 21 ++++++++ 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/packages/rapidoc/src/components/schema-table.ts b/packages/rapidoc/src/components/schema-table.ts index b6d68dc3..8c9c903f 100644 --- a/packages/rapidoc/src/components/schema-table.ts +++ b/packages/rapidoc/src/components/schema-table.ts @@ -409,18 +409,29 @@ export default class SchemaTable extends SchemaBaseElement { ${this.renderDescrCell(node.description)}
- ${node.options?.map( - (opt, i) => html` -
-
- OPT ${opt.optionIndex || i + 1} - ${opt.optionTitle ? html`${opt.optionTitle}` : ""} -
-
-
-
-
${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)}
- `, + ${node.options?.map((opt, i) => + opt.kind === "primitive" + ? // A primitive option fits on a single row: the badge, then its type and description + html`
+ ${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)} +
` + : html` +
+
+ OPT ${opt.optionIndex || i + 1} + ${ + opt.optionTitle + ? html`${opt.optionTitle}` + : "" + } +
+
+
+
+
+ ${this.renderAST(opt, "xxx-of-option", schemaLevel + 1, indentLevel + 1)} +
+ `, )}
`; @@ -429,11 +440,26 @@ export default class SchemaTable extends SchemaBaseElement { // 4. Primitive kind if (node.kind === "primitive") { const dataTypeCss = this.getDataTypeCss(node.type); + // Primitive option of a oneOf / anyOf: the OPT n badge (and the title) takes the place of the name + const isOption = parentType === "xxx-of-option" && !!node.optionIndex; return html`
- ${deprecatedIcon}${node.name ? html`${node.name}` : ""} + ${ + isOption + ? html`OPT ${node.optionIndex}${ + node.optionTitle + ? html`${node.optionTitle}` + : "" + }` + : "" + }${deprecatedIcon}${ + node.name && !isOption + ? html`${node.name}` + : "" + } ${node.required ? html`*` : ""}
diff --git a/packages/rapidoc/src/utils/schema-utils.js b/packages/rapidoc/src/utils/schema-utils.js index a68380fd..ac852bb5 100644 --- a/packages/rapidoc/src/utils/schema-utils.js +++ b/packages/rapidoc/src/utils/schema-utils.js @@ -1519,6 +1519,10 @@ function composeSchemaAST(schema, level, name, isRequired, suffix) { optAst.writeOnly = false; optAst.optionIndex = idx + 1; optAst.optionTitle = optTitle; + if (optAst.kind === "primitive" && !optTitle) { + // The OPT n badge already names it, a row labelled "Option n" would only repeat it + optAst.name = ""; + } } return optAst; }) diff --git a/tests/unit/schema-ast.test.js b/tests/unit/schema-ast.test.js index 7d99b402..b4eab7ad 100644 --- a/tests/unit/schema-ast.test.js +++ b/tests/unit/schema-ast.test.js @@ -414,6 +414,27 @@ describe("schemaToAST", () => { }); }); + describe("oneOf options", () => { + it("should not name the untitled primitive options, but keep the name of the other options", () => { + const ast = schemaToAST({ + oneOf: [ + { type: "string" }, + { type: "number", title: "Count" }, + { type: "object", properties: { id: { type: "string" } } }, + ], + }); + assert.strictEqual(ast.kind, "union"); + assert.deepStrictEqual( + ast.options.map((o) => [o.optionIndex, o.name]), + [ + [1, ""], + [2, "Count"], + [3, "Option 3"], + ], + ); + }); + }); + describe("recursion limit & null inputs", () => { it("should return null for falsy schema input", () => { assert.strictEqual(schemaToAST(null), null);