From feb064ba248bcfe2ba4ab9e979490c95cc632ff4 Mon Sep 17 00:00:00 2001 From: nimbly1 <258366730+nimbly1@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:55:55 -0500 Subject: [PATCH 1/5] fix: separate multiple schema examples when displaying them getTypeInfo returned the `examples` array of a schema unchanged, and the schema table rendered it as a template value, which joins the entries with nothing between them ("applebananapear"). Join the entries with the same separator the Allowed row uses, writing non-string examples as JSON. A single `example` is left as it was, since an array there is one value. Fixes #316 Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + src/utils/schema-utils.js | 4 ++- tests/schema-utils.examples.test.js | 44 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/schema-utils.examples.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6b8d7b..fb916343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ This package follows standard semver, `..`. No breaking cha ## 2.4 * Add support for [OpenAPI Spec 3.2](https://www.openapis.org/blog/2025/09/23/announcing-openapi-v3-2). +* Separate multiple `examples` of a schema with `┃` when they are displayed, instead of running them together. ## 2.3 * Schema types will now utilize the title property for displaying, rather than the type ID, when defined. diff --git a/src/utils/schema-utils.js b/src/utils/schema-utils.js index a1ebb4e4..10178467 100644 --- a/src/utils/schema-utils.js +++ b/src/utils/schema-utils.js @@ -35,7 +35,9 @@ export function getTypeInfo(parameter, options = { includeNulls: false, enableEx } } - const examples = schema.examples || schema.example || options?.enableExampleGeneration && getSampleValueByType(schema, null) || ''; + // The examples of OpenAPI 3.1 are a list, which would be displayed without anything between the entries + const examplesList = Array.isArray(schema.examples) ? schema.examples.map(e => typeof e === 'string' ? e : JSON.stringify(e)).join(' ┃ ') : schema.examples; + const examples = examplesList || schema.example || options?.enableExampleGeneration && getSampleValueByType(schema, null) || ''; const info = { type: dataType, format, diff --git a/tests/schema-utils.examples.test.js b/tests/schema-utils.examples.test.js new file mode 100644 index 00000000..54b3b51a --- /dev/null +++ b/tests/schema-utils.examples.test.js @@ -0,0 +1,44 @@ +import test from 'ava'; +import { expect } from 'chai'; + +import { getTypeInfo } from '../src/utils/schema-utils.js'; + +test('schema-utils.js getTypeInfo separates multiple examples', t => { + const info = getTypeInfo({ type: 'string', examples: ['apple', 'banana', 'pear'] }); + + expect(info.example).to.equal('apple ┃ banana ┃ pear'); + t.pass(); +}); + +test('schema-utils.js getTypeInfo keeps a single entry of examples as is', t => { + expect(getTypeInfo({ type: 'string', examples: ['apple'] }).example).to.equal('apple'); + t.pass(); +}); + +test('schema-utils.js getTypeInfo writes non-string examples as JSON', t => { + const info = getTypeInfo({ type: 'integer', examples: [1, 2, 3] }); + expect(info.example).to.equal('1 ┃ 2 ┃ 3'); + + const objects = getTypeInfo({ type: 'object', examples: [{ id: 1 }, { id: 2 }] }); + expect(objects.example).to.equal('{"id":1} ┃ {"id":2}'); + t.pass(); +}); + +test('schema-utils.js getTypeInfo falls back to example when examples is empty', t => { + const info = getTypeInfo({ type: 'string', examples: [], example: 'apple' }); + + expect(info.example).to.equal('apple'); + t.pass(); +}); + +test('schema-utils.js getTypeInfo has no example for an empty examples list', t => { + expect(getTypeInfo({ type: 'string', examples: [] }).example).to.equal(''); + t.pass(); +}); + +test('schema-utils.js getTypeInfo does not change a single example', t => { + expect(getTypeInfo({ type: 'string', example: 'apple' }).example).to.equal('apple'); + // a single example of an array type is one value, so it is left for the caller to display + expect(getTypeInfo({ type: 'array', items: { type: 'string' }, example: ['a', 'b'] }).example).to.deep.equal(['a', 'b']); + t.pass(); +}); From 7ec9e50196434e471bd7483edb47844dd07d01cd Mon Sep 17 00:00:00 2001 From: nimbly1 <258366730+nimbly1@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:20:43 -0500 Subject: [PATCH 2/5] fix: format the examples of a schema where they are displayed getTypeInfo is the schema evaluator, so it should not build display text. The previous commit made it join the entries of `examples` into a string. getTypeInfo is now back to what it was before that commit and returns the examples of a schema as the schema has them (a list stays a list). The views that show the example (schema table, schema tree, response header table and request form table) now separate the entries of a list with the new formatExamples helper in common-utils, in the same way they already separate the allowed values, writing entries that are not text as JSON. The text shown for a list of `examples` is unchanged from the previous commit. Two differences from the previous commit remain. A single `example` that is itself an array is now shown separated the same way, because the views cannot tell it from a list of examples (before, it was shown run together). The placeholders and click-to-fill values of the request inputs are not touched, so they are again what they were before the previous commit. An empty `examples` list is again handled as it was before the previous commit. Co-Authored-By: Claude Sonnet 5.5 --- src/components/api-response.js | 4 +-- src/components/request-form-table.js | 4 +-- src/components/schema-table.js | 4 +-- src/components/schema-tree.js | 4 +-- src/utils/common-utils.js | 5 ++++ src/utils/schema-utils.js | 4 +-- tests/common-utils.test.js | 37 ++++++++++++++++++++++++++++ tests/schema-utils.examples.test.js | 37 +++++++++++----------------- 8 files changed, 65 insertions(+), 34 deletions(-) create mode 100644 tests/common-utils.test.js diff --git a/src/components/api-response.js b/src/components/api-response.js index f5e3ecf9..bdd0576e 100644 --- a/src/components/api-response.js +++ b/src/components/api-response.js @@ -1,7 +1,7 @@ import { LitElement, html, css } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { schemaInObjectNotation, generateExample, getTypeInfo } from '../utils/schema-utils.js'; -import { toMarkdown } from '../utils/common-utils.js'; +import { toMarkdown, formatExamples } from '../utils/common-utils.js'; import { getI18nText } from '../languages/index.js'; import FontStyles from '../styles/font-styles.js'; import FlexStyles from '../styles/flex-styles.js'; @@ -240,7 +240,7 @@ export default class ApiResponse extends LitElement {
${unsafeHTML(toMarkdown(v.description || ''))}
- ${typeData?.example ?? typeData?.default ?? ''} + ${formatExamples(typeData?.example) ?? typeData?.default ?? ''} `; diff --git a/src/components/request-form-table.js b/src/components/request-form-table.js index 0148e129..20bc2c16 100644 --- a/src/components/request-form-table.js +++ b/src/components/request-form-table.js @@ -1,6 +1,6 @@ /* eslint-disable indent */ import { html } from 'lit'; -import { toMarkdown } from '../utils/common-utils.js'; +import { toMarkdown, formatExamples } from '../utils/common-utils.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { isPatternProperty } from '../utils/schema-utils.js'; import { map } from 'lit/directives/map.js'; @@ -156,7 +156,7 @@ function generatePrimitiveRow(rowData, parentRecursionOptions) { } this.computeCurlSyntax(); }}"> - ${type === 'array' ? example.join(', ') : example} + ${type === 'array' ? example.join(', ') : formatExamples(example)} ${type === 'array' ? '] ' : ''} ` diff --git a/src/components/schema-table.js b/src/components/schema-table.js index 4ea72a23..32fffbd5 100644 --- a/src/components/schema-table.js +++ b/src/components/schema-table.js @@ -1,5 +1,5 @@ import { LitElement, html, css } from 'lit'; -import { toMarkdown } from '../utils/common-utils.js'; +import { toMarkdown, formatExamples } from '../utils/common-utils.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import FontStyles from '../styles/font-styles.js'; import SchemaStyles from '../styles/schema-styles.js'; @@ -322,7 +322,7 @@ export default class SchemaTable extends LitElement { ${defaultValue !== '' ? html`
Default: ${defaultValue}

` : ''} ${allowedValues ? html`
Allowed: ${allowedValues.filter(v => v !== null && v !== undefined).join(' ┃ ')}

` : ''} ${pattern ? html`
Pattern: ${pattern}

` : ''} - ${example ? html`
Example: ${example}

` : ''} + ${example ? html`
Example: ${formatExamples(example)}

` : ''} `; diff --git a/src/components/schema-tree.js b/src/components/schema-tree.js index 97cca90c..ee6efea0 100644 --- a/src/components/schema-tree.js +++ b/src/components/schema-tree.js @@ -1,5 +1,5 @@ import { LitElement, html, css } from 'lit'; -import { toMarkdown } from '../utils/common-utils.js'; +import { toMarkdown, formatExamples } from '../utils/common-utils.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { getI18nText } from '../languages/index.js'; import FontStyles from '../styles/font-styles.js'; @@ -315,7 +315,7 @@ export default class SchemaTree extends LitElement { ${defaultValue !== '' ? html`
Default: ${defaultValue}

` : ''} ${allowedValues ? html`
Allowed: ${allowedValues.filter(v => v !== null && v !== undefined).join(' ┃ ')}

` : ''} ${pattern ? html`
Pattern: ${pattern}

` : ''} - ${example ? html`
Example: ${example}

` : ''}` : ''} + ${example ? html`
Example: ${formatExamples(example)}

` : ''}` : ''} `; diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index a55e96c5..6706cf0d 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -163,6 +163,11 @@ export function toMarkdown(markdownStringRaw) { return markdownResult; } +// The examples of a schema are a list (OpenAPI 3.1), so separate them in the same way the allowed values are, with any that are not text written as JSON +export function formatExamples(examples) { + return Array.isArray(examples) ? examples.map(e => typeof e === 'string' ? e : JSON.stringify(e)).join(' ┃ ') : examples; +} + export function getSanitizedUrl(urlString) { if (!urlString) { return ''; diff --git a/src/utils/schema-utils.js b/src/utils/schema-utils.js index 10178467..a1ebb4e4 100644 --- a/src/utils/schema-utils.js +++ b/src/utils/schema-utils.js @@ -35,9 +35,7 @@ export function getTypeInfo(parameter, options = { includeNulls: false, enableEx } } - // The examples of OpenAPI 3.1 are a list, which would be displayed without anything between the entries - const examplesList = Array.isArray(schema.examples) ? schema.examples.map(e => typeof e === 'string' ? e : JSON.stringify(e)).join(' ┃ ') : schema.examples; - const examples = examplesList || schema.example || options?.enableExampleGeneration && getSampleValueByType(schema, null) || ''; + const examples = schema.examples || schema.example || options?.enableExampleGeneration && getSampleValueByType(schema, null) || ''; const info = { type: dataType, format, diff --git a/tests/common-utils.test.js b/tests/common-utils.test.js new file mode 100644 index 00000000..ac698248 --- /dev/null +++ b/tests/common-utils.test.js @@ -0,0 +1,37 @@ +import test from 'ava'; +import { expect } from 'chai'; + +import { formatExamples } from '../src/utils/common-utils.js'; + +test('common-utils.js formatExamples separates multiple examples', t => { + expect(formatExamples(['apple', 'banana', 'pear'])).to.equal('apple ┃ banana ┃ pear'); + t.pass(); +}); + +test('common-utils.js formatExamples keeps a single entry as is', t => { + expect(formatExamples(['apple'])).to.equal('apple'); + t.pass(); +}); + +test('common-utils.js formatExamples writes examples which are not text as JSON', t => { + expect(formatExamples([1, 2, 3])).to.equal('1 ┃ 2 ┃ 3'); + expect(formatExamples([true, null])).to.equal('true ┃ null'); + expect(formatExamples([{ id: 1 }, { id: 2 }])).to.equal('{"id":1} ┃ {"id":2}'); + expect(formatExamples([['a', 'b'], ['c']])).to.equal('["a","b"] ┃ ["c"]'); + t.pass(); +}); + +test('common-utils.js formatExamples gives an empty list no text', t => { + expect(formatExamples([])).to.equal(''); + t.pass(); +}); + +test('common-utils.js formatExamples does not change a single example', t => { + expect(formatExamples('apple')).to.equal('apple'); + expect(formatExamples(5)).to.equal(5); + expect(formatExamples('')).to.equal(''); + expect(formatExamples(undefined)).to.equal(undefined); + const example = { id: 1 }; + expect(formatExamples(example)).to.equal(example); + t.pass(); +}); diff --git a/tests/schema-utils.examples.test.js b/tests/schema-utils.examples.test.js index 54b3b51a..1d0512ba 100644 --- a/tests/schema-utils.examples.test.js +++ b/tests/schema-utils.examples.test.js @@ -3,36 +3,22 @@ import { expect } from 'chai'; import { getTypeInfo } from '../src/utils/schema-utils.js'; -test('schema-utils.js getTypeInfo separates multiple examples', t => { - const info = getTypeInfo({ type: 'string', examples: ['apple', 'banana', 'pear'] }); - - expect(info.example).to.equal('apple ┃ banana ┃ pear'); - t.pass(); -}); - -test('schema-utils.js getTypeInfo keeps a single entry of examples as is', t => { - expect(getTypeInfo({ type: 'string', examples: ['apple'] }).example).to.equal('apple'); +test('schema-utils.js getTypeInfo returns the examples of a schema as they are', t => { + expect(getTypeInfo({ type: 'string', examples: ['apple', 'banana', 'pear'] }).example).to.deep.equal(['apple', 'banana', 'pear']); + expect(getTypeInfo({ type: 'integer', examples: [1, 2, 3] }).example).to.deep.equal([1, 2, 3]); + expect(getTypeInfo({ type: 'object', examples: [{ id: 1 }, { id: 2 }] }).example).to.deep.equal([{ id: 1 }, { id: 2 }]); t.pass(); }); -test('schema-utils.js getTypeInfo writes non-string examples as JSON', t => { - const info = getTypeInfo({ type: 'integer', examples: [1, 2, 3] }); - expect(info.example).to.equal('1 ┃ 2 ┃ 3'); - - const objects = getTypeInfo({ type: 'object', examples: [{ id: 1 }, { id: 2 }] }); - expect(objects.example).to.equal('{"id":1} ┃ {"id":2}'); +test('schema-utils.js getTypeInfo keeps a single entry of examples as a list', t => { + expect(getTypeInfo({ type: 'string', examples: ['apple'] }).example).to.deep.equal(['apple']); t.pass(); }); -test('schema-utils.js getTypeInfo falls back to example when examples is empty', t => { - const info = getTypeInfo({ type: 'string', examples: [], example: 'apple' }); +test('schema-utils.js getTypeInfo hands the examples to the schema views as a list', t => { + const info = getTypeInfo({ type: 'string', examples: ['apple', 'banana'] }); - expect(info.example).to.equal('apple'); - t.pass(); -}); - -test('schema-utils.js getTypeInfo has no example for an empty examples list', t => { - expect(getTypeInfo({ type: 'string', examples: [] }).example).to.equal(''); + expect(JSON.parse(info.html).example).to.deep.equal(['apple', 'banana']); t.pass(); }); @@ -42,3 +28,8 @@ test('schema-utils.js getTypeInfo does not change a single example', t => { expect(getTypeInfo({ type: 'array', items: { type: 'string' }, example: ['a', 'b'] }).example).to.deep.equal(['a', 'b']); t.pass(); }); + +test('schema-utils.js getTypeInfo prefers the examples over a single example', t => { + expect(getTypeInfo({ type: 'string', examples: ['apple', 'banana'], example: 'pear' }).example).to.deep.equal(['apple', 'banana']); + t.pass(); +}); From a7cfea5b5d31700c65bbea99340af9d16f0a10eb Mon Sep 17 00:00:00 2001 From: nimbly1 <258366730+nimbly1@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:15:35 -0500 Subject: [PATCH 3/5] fix: keep a legacy example as one entry in the examples list getTypeInfo returns a list. Schema examples stay a list, a legacy example is one entry so an array value is not split, and a missing example is an empty list. The views render that list, and the request form still fills and placeholders from the first example. --- src/components/api-request.js | 15 +++++++--- src/components/request-form-table.js | 19 ++++++------ src/components/schema-table.js | 2 +- src/components/schema-tree.js | 2 +- src/utils/common-utils.js | 23 +++++++++++++-- src/utils/schema-utils.js | 43 +++++++++++++++++++++++++++- tests/common-utils.test.js | 22 ++++++++++---- tests/schema-utils.examples.test.js | 38 +++++++++++++++++++++--- 8 files changed, 137 insertions(+), 27 deletions(-) diff --git a/src/components/api-request.js b/src/components/api-request.js index c08cc984..e0d214ff 100644 --- a/src/components/api-request.js +++ b/src/components/api-request.js @@ -6,13 +6,20 @@ import formatXml from 'xml-but-prettier'; import { getI18nText } from '../languages/index.js'; import { schemaInObjectNotation, getTypeInfo, generateExample, isPatternProperty } from '../utils/schema-utils.js'; -import { toMarkdown } from '../utils/common-utils.js'; +import { toMarkdown, exampleText } from '../utils/common-utils.js'; import './schema-tree.js'; import getRequestFormTable from './request-form-table.js'; import './tag-input.js'; import './syntax-highlighter.js'; import json5 from 'json5'; +function exampleMap(example) { + if (!Array.isArray(example) || example.length === 0) { + return undefined; + } + return Object.fromEntries(example.map((value, index) => [index === 0 ? 'Example' : `Example${index + 1}`, { value }])); +} + const textFileRegex = RegExp('^font/|tar$|zip$|7z$|rtf$|msword$|excel$|/pdf$|/octet-stream$|^application/vnd.'); const mediaFileRegex = RegExp('^audio/|^image/|^video/'); @@ -202,7 +209,7 @@ export default class ApiRequest extends LitElement { data-param-serialize-style = "${paramStyle}" data-param-serialize-explode = "${paramExplode}" spellcheck = "false" - placeholder="${generatedParamSchema.example || defaultVal || ''}" + placeholder="${exampleText(generatedParamSchema.example) || defaultVal || ''}" style = "width:100%; margin-top: 1rem; margin-bottom: 1rem;" .value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}">` || generatedParamSchema.allowedValues && html` @@ -224,7 +231,7 @@ export default class ApiRequest extends LitElement { id="request-param-${paramName}" aria-labelledby = "request-${paramName}-label" @input="${() => { this.computeCurlSyntax(); }}" - placeholder="${generatedParamSchema.example || defaultVal || ''}" + placeholder="${exampleText(generatedParamSchema.example) || defaultVal || ''}" class="request-param" part="textbox textbox-param" data-ptype="${paramLocation}" @@ -354,7 +361,7 @@ export default class ApiRequest extends LitElement { const paramSchema = getTypeInfo(param, { includeNulls: this.includeNulls }); const examples = generateExample( - param.examples || param.example && { Example: { value: param.example } } || paramSchema.examples || paramSchema.example && { Example: { value: paramSchema.example } }, + param.examples || param.example && { Example: { value: param.example } } || exampleMap(paramSchema.example), null, param.schema, null, false, true, 'json', false); const someExampleWithSummaryOrDescription = examples.some((x) => x.exampleSummary?.length > 0 || x.exampleDescription?.length > 0); diff --git a/src/components/request-form-table.js b/src/components/request-form-table.js index 20bc2c16..28efe380 100644 --- a/src/components/request-form-table.js +++ b/src/components/request-form-table.js @@ -78,6 +78,11 @@ function generateFormRows(data, options, dataType = 'object', key = '', descript function generatePrimitiveRow(rowData, parentRecursionOptions) { const { type, format, readOrWriteOnly, constraints, defaultValue, example, allowedValues, pattern, schemaDescription, schemaTitle, deprecated } = rowData; const { key, keyLabel, keyDescr, description, dataType, isRequired, options } = parentRecursionOptions; + const examples = Array.isArray(example) ? example : (example !== null && example !== undefined && example !== '' ? [example] : []); + const filledExample = examples.length ? examples[0] : undefined; + const exampleFill = type === 'array' && Array.isArray(filledExample) + ? filledExample.join('~|~') + : (filledExample === null || filledExample === undefined ? '' : (typeof filledExample === 'string' ? filledExample : JSON.stringify(filledExample))); if (readOrWriteOnly === '🆁') { return undefined; } @@ -106,8 +111,8 @@ function generatePrimitiveRow(rowData, parentRecursionOptions) { ${inputFieldKeyLabel.call(this, key.startsWith('::OPTION'), keyLabel, keyDescr, dataType, deprecated, isRequired, schemaTitle, format || type, rowGenerator)} - ${dataType === 'array' ? getArrayFormField.call(this, keyLabel, example, defaultValue, format, rowGenerator) : ''} - ${dataType !== 'array' ? getPrimitiveFormField.call(this, keyLabel, example, defaultValue, format, options, rowGenerator) : ''} + ${dataType === 'array' ? getArrayFormField.call(this, keyLabel, filledExample, defaultValue, format, rowGenerator) : ''} + ${dataType !== 'array' ? getPrimitiveFormField.call(this, keyLabel, exampleFill, defaultValue, format, options, rowGenerator) : ''} ${description ? html`
${unsafeHTML(toMarkdown(description))}
` : ''} ${defaultValue || constraints || allowedValues || pattern @@ -138,17 +143,16 @@ function generatePrimitiveRow(rowData, parentRecursionOptions) { - ${schemaDescription || example ? html` + ${schemaDescription || examples.length ? html` ${unsafeHTML(toMarkdown(schemaDescription || ''))} - ${example + ${examples.length ? html` Example: - ${type === 'array' ? '[ ' : ''} - ${type === 'array' ? example.join(', ') : formatExamples(example)} + ${formatExamples(examples)} - ${type === 'array' ? '] ' : ''} ` : ''} diff --git a/src/components/schema-table.js b/src/components/schema-table.js index 32fffbd5..5cea540b 100644 --- a/src/components/schema-table.js +++ b/src/components/schema-table.js @@ -322,7 +322,7 @@ export default class SchemaTable extends LitElement { ${defaultValue !== '' ? html`
Default: ${defaultValue}

` : ''} ${allowedValues ? html`
Allowed: ${allowedValues.filter(v => v !== null && v !== undefined).join(' ┃ ')}

` : ''} ${pattern ? html`
Pattern: ${pattern}

` : ''} - ${example ? html`
Example: ${formatExamples(example)}

` : ''} + ${(Array.isArray(example) ? example.length : example) ? html`
Example: ${formatExamples(example)}

` : ''} `; diff --git a/src/components/schema-tree.js b/src/components/schema-tree.js index ee6efea0..799c4b9b 100644 --- a/src/components/schema-tree.js +++ b/src/components/schema-tree.js @@ -315,7 +315,7 @@ export default class SchemaTree extends LitElement { ${defaultValue !== '' ? html`
Default: ${defaultValue}

` : ''} ${allowedValues ? html`
Allowed: ${allowedValues.filter(v => v !== null && v !== undefined).join(' ┃ ')}

` : ''} ${pattern ? html`
Pattern: ${pattern}

` : ''} - ${example ? html`
Example: ${formatExamples(example)}

` : ''}` : ''} + ${(Array.isArray(example) ? example.length : example) ? html`
Example: ${formatExamples(example)}

` : ''}` : ''} `; diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index 6706cf0d..32bdceb3 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -163,9 +163,28 @@ export function toMarkdown(markdownStringRaw) { return markdownResult; } -// The examples of a schema are a list (OpenAPI 3.1), so separate them in the same way the allowed values are, with any that are not text written as JSON +function displayExample(entry) { + return typeof entry === 'string' ? entry : JSON.stringify(entry); +} + +// `examples` is the list from getTypeInfo. A value that is not a list is one legacy example. export function formatExamples(examples) { - return Array.isArray(examples) ? examples.map(e => typeof e === 'string' ? e : JSON.stringify(e)).join(' ┃ ') : examples; + if (Array.isArray(examples)) { + return examples.map(displayExample).join(' ┃ '); + } + if (examples === null || examples === undefined || examples === '') { + return ''; + } + return displayExample(examples); +} + +// Placeholder text is the first example. An empty list has none. +export function exampleText(examples) { + const value = Array.isArray(examples) ? examples[0] : examples; + if (value === null || value === undefined || value === '') { + return ''; + } + return displayExample(value); } export function getSanitizedUrl(urlString) { diff --git a/src/utils/schema-utils.js b/src/utils/schema-utils.js index a1ebb4e4..3f19a634 100644 --- a/src/utils/schema-utils.js +++ b/src/utils/schema-utils.js @@ -7,6 +7,47 @@ import xmlFormatter from './xml/xml.js'; const IS_MISSING_TYPE_INFO_TYPE = ''; const EXAMPLE_VALUE_FOR_MISSING_TYPE = ''; +// OpenAPI 3.1 `examples` is a list. A legacy `example` is one value, even when that value is an array. +// An examples map (OpenAPI 3.0) contributes each entry's value. No example is an empty list. +function schemaExampleList(schema, options) { + const listed = listedExamples(schema.examples); + if (listed) { + return listed; + } + if (Object.prototype.hasOwnProperty.call(schema, 'example') && schema.example !== undefined) { + return [schema.example]; + } + if (options?.enableExampleGeneration) { + const generated = getSampleValueByType(schema, null); + if (generated !== undefined && generated !== '') { + return [generated]; + } + } + return []; +} + +function listedExamples(examples) { + if (Array.isArray(examples)) { + return examples.length ? examples : null; + } + if (!examples || typeof examples !== 'object') { + return null; + } + const values = []; + for (const entry of Object.values(examples)) { + if (entry && typeof entry === 'object') { + if (Object.prototype.hasOwnProperty.call(entry, 'value') && entry.value !== undefined) { + values.push(entry.value); + } + continue; + } + if (entry !== undefined) { + values.push(entry); + } + } + return values.length ? values : null; +} + /* Generates an schema object containing type and constraint info */ export function getTypeInfo(parameter, options = { includeNulls: false, enableExampleGeneration: false }) { if (!parameter) { @@ -35,7 +76,7 @@ export function getTypeInfo(parameter, options = { includeNulls: false, enableEx } } - const examples = schema.examples || schema.example || options?.enableExampleGeneration && getSampleValueByType(schema, null) || ''; + const examples = schemaExampleList(schema, options); const info = { type: dataType, format, diff --git a/tests/common-utils.test.js b/tests/common-utils.test.js index ac698248..1230cd19 100644 --- a/tests/common-utils.test.js +++ b/tests/common-utils.test.js @@ -1,7 +1,7 @@ import test from 'ava'; import { expect } from 'chai'; -import { formatExamples } from '../src/utils/common-utils.js'; +import { exampleText, formatExamples } from '../src/utils/common-utils.js'; test('common-utils.js formatExamples separates multiple examples', t => { expect(formatExamples(['apple', 'banana', 'pear'])).to.equal('apple ┃ banana ┃ pear'); @@ -26,12 +26,22 @@ test('common-utils.js formatExamples gives an empty list no text', t => { t.pass(); }); -test('common-utils.js formatExamples does not change a single example', t => { +test('common-utils.js formatExamples prints one legacy example as text', t => { expect(formatExamples('apple')).to.equal('apple'); - expect(formatExamples(5)).to.equal(5); + expect(formatExamples(5)).to.equal('5'); expect(formatExamples('')).to.equal(''); - expect(formatExamples(undefined)).to.equal(undefined); - const example = { id: 1 }; - expect(formatExamples(example)).to.equal(example); + expect(formatExamples(undefined)).to.equal(''); + expect(formatExamples({ id: 1 })).to.equal('{"id":1}'); + expect(formatExamples([['a', 'b']])).to.equal('["a","b"]'); + t.pass(); +}); + +test('common-utils.js exampleText uses the first example for a placeholder', t => { + expect(exampleText(['apple', 'banana'])).to.equal('apple'); + expect(exampleText([['a', 'b']])).to.equal('["a","b"]'); + expect(exampleText([0])).to.equal('0'); + expect(exampleText([false])).to.equal('false'); + expect(exampleText([])).to.equal(''); + expect(exampleText(undefined)).to.equal(''); t.pass(); }); diff --git a/tests/schema-utils.examples.test.js b/tests/schema-utils.examples.test.js index 1d0512ba..ebaed210 100644 --- a/tests/schema-utils.examples.test.js +++ b/tests/schema-utils.examples.test.js @@ -22,10 +22,12 @@ test('schema-utils.js getTypeInfo hands the examples to the schema views as a li t.pass(); }); -test('schema-utils.js getTypeInfo does not change a single example', t => { - expect(getTypeInfo({ type: 'string', example: 'apple' }).example).to.equal('apple'); - // a single example of an array type is one value, so it is left for the caller to display - expect(getTypeInfo({ type: 'array', items: { type: 'string' }, example: ['a', 'b'] }).example).to.deep.equal(['a', 'b']); +test('schema-utils.js getTypeInfo wraps a legacy example as one entry', t => { + expect(getTypeInfo({ type: 'string', example: 'apple' }).example).to.deep.equal(['apple']); + // The example value is an array, so the list has one entry and the value stays intact + expect(getTypeInfo({ type: 'array', items: { type: 'string' }, example: ['a', 'b'] }).example).to.deep.equal([['a', 'b']]); + expect(getTypeInfo({ type: 'integer', example: 0 }).example).to.deep.equal([0]); + expect(getTypeInfo({ type: 'boolean', example: false }).example).to.deep.equal([false]); t.pass(); }); @@ -33,3 +35,31 @@ test('schema-utils.js getTypeInfo prefers the examples over a single example', t expect(getTypeInfo({ type: 'string', examples: ['apple', 'banana'], example: 'pear' }).example).to.deep.equal(['apple', 'banana']); t.pass(); }); + +test('schema-utils.js getTypeInfo uses a legacy example when the examples list is empty', t => { + expect(getTypeInfo({ type: 'string', examples: [], example: 'pear' }).example).to.deep.equal(['pear']); + t.pass(); +}); + +test('schema-utils.js getTypeInfo reads values from an examples map', t => { + const info = getTypeInfo({ + type: 'string', + examples: { red: { value: 'red' }, blue: { value: 'blue' }, none: { summary: 'missing value' } } + }); + + expect(info.example).to.deep.equal(['red', 'blue']); + t.pass(); +}); + +test('schema-utils.js getTypeInfo returns an empty list when a schema has no example', t => { + expect(getTypeInfo({ type: 'string' }).example).to.deep.equal([]); + expect(JSON.parse(getTypeInfo({ type: 'string' }).html).example).to.deep.equal([]); + t.pass(); +}); + +test('schema-utils.js getTypeInfo generates one example only when asked and none is present', t => { + expect(getTypeInfo({ type: 'string' }, { enableExampleGeneration: true }).example).to.deep.equal(['string']); + expect(getTypeInfo({ type: 'boolean' }, { enableExampleGeneration: true }).example).to.deep.equal([false]); + expect(getTypeInfo({ type: 'string', example: 'apple' }, { enableExampleGeneration: true }).example).to.deep.equal(['apple']); + t.pass(); +}); From 00da152d44eb2b94a7c0d74300ab95c0601e2ac8 Mon Sep 17 00:00:00 2001 From: nimbly1 <258366730+nimbly1@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:20:20 -0500 Subject: [PATCH 4/5] fix: follow the review notes on example display Remove the Example map from the parameter renderer. Show an example when formatExamples returns text, and rename the helper to convertToDisplayString. --- src/components/api-request.js | 9 +-------- src/components/request-form-table.js | 10 +++++----- src/components/schema-table.js | 3 ++- src/components/schema-tree.js | 3 ++- src/utils/common-utils.js | 10 ++++------ 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/components/api-request.js b/src/components/api-request.js index e0d214ff..47f6561d 100644 --- a/src/components/api-request.js +++ b/src/components/api-request.js @@ -13,13 +13,6 @@ import './tag-input.js'; import './syntax-highlighter.js'; import json5 from 'json5'; -function exampleMap(example) { - if (!Array.isArray(example) || example.length === 0) { - return undefined; - } - return Object.fromEntries(example.map((value, index) => [index === 0 ? 'Example' : `Example${index + 1}`, { value }])); -} - const textFileRegex = RegExp('^font/|tar$|zip$|7z$|rtf$|msword$|excel$|/pdf$|/octet-stream$|^application/vnd.'); const mediaFileRegex = RegExp('^audio/|^image/|^video/'); @@ -361,7 +354,7 @@ export default class ApiRequest extends LitElement { const paramSchema = getTypeInfo(param, { includeNulls: this.includeNulls }); const examples = generateExample( - param.examples || param.example && { Example: { value: param.example } } || exampleMap(paramSchema.example), + param.examples || param.example && { Example: { value: param.example } } || paramSchema.examples || paramSchema.example && { Example: { value: paramSchema.example } }, null, param.schema, null, false, true, 'json', false); const someExampleWithSummaryOrDescription = examples.some((x) => x.exampleSummary?.length > 0 || x.exampleDescription?.length > 0); diff --git a/src/components/request-form-table.js b/src/components/request-form-table.js index 28efe380..c1fe51be 100644 --- a/src/components/request-form-table.js +++ b/src/components/request-form-table.js @@ -78,8 +78,8 @@ function generateFormRows(data, options, dataType = 'object', key = '', descript function generatePrimitiveRow(rowData, parentRecursionOptions) { const { type, format, readOrWriteOnly, constraints, defaultValue, example, allowedValues, pattern, schemaDescription, schemaTitle, deprecated } = rowData; const { key, keyLabel, keyDescr, description, dataType, isRequired, options } = parentRecursionOptions; - const examples = Array.isArray(example) ? example : (example !== null && example !== undefined && example !== '' ? [example] : []); - const filledExample = examples.length ? examples[0] : undefined; + const exampleLabel = formatExamples(example); + const filledExample = Array.isArray(example) ? example[0] : example; const exampleFill = type === 'array' && Array.isArray(filledExample) ? filledExample.join('~|~') : (filledExample === null || filledExample === undefined ? '' : (typeof filledExample === 'string' ? filledExample : JSON.stringify(filledExample))); @@ -143,11 +143,11 @@ function generatePrimitiveRow(rowData, parentRecursionOptions) { - ${schemaDescription || examples.length ? html` + ${schemaDescription || exampleLabel ? html` ${unsafeHTML(toMarkdown(schemaDescription || ''))} - ${examples.length + ${exampleLabel ? html` Example: - ${formatExamples(examples)} + ${exampleLabel} ` : ''} diff --git a/src/components/schema-table.js b/src/components/schema-table.js index 5cea540b..ea82f07e 100644 --- a/src/components/schema-table.js +++ b/src/components/schema-table.js @@ -293,6 +293,7 @@ export default class SchemaTable extends LitElement { // For Primitive Data types const { type, cssType, format, readOrWriteOnly, constraints, defaultValue, example, allowedValues, pattern, schemaDescription, schemaTitle, deprecated } = JSON.parse(data); + const exampleLabel = formatExamples(example); if (readOrWriteOnly === '🆁' && this.schemaHideReadOnly === 'true') { return { result: undefined, keyLabelMaxCharacterLength: newIndentLevel }; } @@ -322,7 +323,7 @@ export default class SchemaTable extends LitElement { ${defaultValue !== '' ? html`
Default: ${defaultValue}

` : ''} ${allowedValues ? html`
Allowed: ${allowedValues.filter(v => v !== null && v !== undefined).join(' ┃ ')}

` : ''} ${pattern ? html`
Pattern: ${pattern}

` : ''} - ${(Array.isArray(example) ? example.length : example) ? html`
Example: ${formatExamples(example)}

` : ''} + ${exampleLabel ? html`
Example: ${exampleLabel}

` : ''} `; diff --git a/src/components/schema-tree.js b/src/components/schema-tree.js index 799c4b9b..99ed9095 100644 --- a/src/components/schema-tree.js +++ b/src/components/schema-tree.js @@ -283,6 +283,7 @@ export default class SchemaTree extends LitElement { // For Primitive Data types const { type, cssType, format, readOrWriteOnly, constraints, defaultValue, example, allowedValues, pattern, schemaDescription, schemaTitle, deprecated } = JSON.parse(data); + const exampleLabel = formatExamples(example); if (readOrWriteOnly === '🆁' && this.schemaHideReadOnly === 'true') { return undefined; } @@ -315,7 +316,7 @@ export default class SchemaTree extends LitElement { ${defaultValue !== '' ? html`
Default: ${defaultValue}

` : ''} ${allowedValues ? html`
Allowed: ${allowedValues.filter(v => v !== null && v !== undefined).join(' ┃ ')}

` : ''} ${pattern ? html`
Pattern: ${pattern}

` : ''} - ${(Array.isArray(example) ? example.length : example) ? html`
Example: ${formatExamples(example)}

` : ''}` : ''} + ${exampleLabel ? html`
Example: ${exampleLabel}

` : ''}` : ''} `; diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index 32bdceb3..6537e3ed 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -163,28 +163,26 @@ export function toMarkdown(markdownStringRaw) { return markdownResult; } -function displayExample(entry) { +function convertToDisplayString(entry) { return typeof entry === 'string' ? entry : JSON.stringify(entry); } -// `examples` is the list from getTypeInfo. A value that is not a list is one legacy example. export function formatExamples(examples) { if (Array.isArray(examples)) { - return examples.map(displayExample).join(' ┃ '); + return examples.map(convertToDisplayString).join(' ┃ '); } if (examples === null || examples === undefined || examples === '') { return ''; } - return displayExample(examples); + return convertToDisplayString(examples); } -// Placeholder text is the first example. An empty list has none. export function exampleText(examples) { const value = Array.isArray(examples) ? examples[0] : examples; if (value === null || value === undefined || value === '') { return ''; } - return displayExample(value); + return convertToDisplayString(value); } export function getSanitizedUrl(urlString) { From 817fc125bd3170346d71319021431cb51218d5ac Mon Sep 17 00:00:00 2001 From: nimbly1 <258366730+nimbly1@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:02:08 -0500 Subject: [PATCH 5/5] fix: always treat the examples of a schema as a list getTypeInfo returns the examples as a list in every case, so the views no longer need to handle a single value. This follows the review notes on the last commit. - Drop the dead `|| ''` after the example list in getTypeInfo. - formatExamples takes a list only. - Export convertToDisplayString and use it for the request form fill value. - Move exampleText into api-request.js, its only user. Co-Authored-By: Claude Sonnet 5.5 --- src/components/api-request.js | 7 ++++++- src/components/api-response.js | 2 +- src/components/request-form-table.js | 6 +++--- src/utils/common-utils.js | 18 ++---------------- src/utils/schema-utils.js | 2 +- tests/common-utils.test.js | 28 ++++++++++++++-------------- 6 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/components/api-request.js b/src/components/api-request.js index 47f6561d..20cdf8e7 100644 --- a/src/components/api-request.js +++ b/src/components/api-request.js @@ -6,7 +6,7 @@ import formatXml from 'xml-but-prettier'; import { getI18nText } from '../languages/index.js'; import { schemaInObjectNotation, getTypeInfo, generateExample, isPatternProperty } from '../utils/schema-utils.js'; -import { toMarkdown, exampleText } from '../utils/common-utils.js'; +import { toMarkdown, convertToDisplayString } from '../utils/common-utils.js'; import './schema-tree.js'; import getRequestFormTable from './request-form-table.js'; import './tag-input.js'; @@ -16,6 +16,11 @@ import json5 from 'json5'; const textFileRegex = RegExp('^font/|tar$|zip$|7z$|rtf$|msword$|excel$|/pdf$|/octet-stream$|^application/vnd.'); const mediaFileRegex = RegExp('^audio/|^image/|^video/'); +// The placeholder of a parameter input is its first example. +function exampleText(examples) { + return convertToDisplayString(examples[0] ?? ''); +} + export default class ApiRequest extends LitElement { createRenderRoot() { return this; } diff --git a/src/components/api-response.js b/src/components/api-response.js index bdd0576e..012a6338 100644 --- a/src/components/api-response.js +++ b/src/components/api-response.js @@ -240,7 +240,7 @@ export default class ApiResponse extends LitElement {
${unsafeHTML(toMarkdown(v.description || ''))}
- ${formatExamples(typeData?.example) ?? typeData?.default ?? ''} + ${formatExamples(typeData?.example ?? []) ?? typeData?.default ?? ''} `; diff --git a/src/components/request-form-table.js b/src/components/request-form-table.js index c1fe51be..4e29e3e8 100644 --- a/src/components/request-form-table.js +++ b/src/components/request-form-table.js @@ -1,6 +1,6 @@ /* eslint-disable indent */ import { html } from 'lit'; -import { toMarkdown, formatExamples } from '../utils/common-utils.js'; +import { toMarkdown, formatExamples, convertToDisplayString } from '../utils/common-utils.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { isPatternProperty } from '../utils/schema-utils.js'; import { map } from 'lit/directives/map.js'; @@ -79,10 +79,10 @@ function generatePrimitiveRow(rowData, parentRecursionOptions) { const { type, format, readOrWriteOnly, constraints, defaultValue, example, allowedValues, pattern, schemaDescription, schemaTitle, deprecated } = rowData; const { key, keyLabel, keyDescr, description, dataType, isRequired, options } = parentRecursionOptions; const exampleLabel = formatExamples(example); - const filledExample = Array.isArray(example) ? example[0] : example; + const [filledExample] = example; const exampleFill = type === 'array' && Array.isArray(filledExample) ? filledExample.join('~|~') - : (filledExample === null || filledExample === undefined ? '' : (typeof filledExample === 'string' ? filledExample : JSON.stringify(filledExample))); + : convertToDisplayString(filledExample ?? ''); if (readOrWriteOnly === '🆁') { return undefined; } diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index 6537e3ed..db7c9b7f 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -163,26 +163,12 @@ export function toMarkdown(markdownStringRaw) { return markdownResult; } -function convertToDisplayString(entry) { +export function convertToDisplayString(entry) { return typeof entry === 'string' ? entry : JSON.stringify(entry); } export function formatExamples(examples) { - if (Array.isArray(examples)) { - return examples.map(convertToDisplayString).join(' ┃ '); - } - if (examples === null || examples === undefined || examples === '') { - return ''; - } - return convertToDisplayString(examples); -} - -export function exampleText(examples) { - const value = Array.isArray(examples) ? examples[0] : examples; - if (value === null || value === undefined || value === '') { - return ''; - } - return convertToDisplayString(value); + return examples.map(convertToDisplayString).join(' ┃ '); } export function getSanitizedUrl(urlString) { diff --git a/src/utils/schema-utils.js b/src/utils/schema-utils.js index 3f19a634..479b0e92 100644 --- a/src/utils/schema-utils.js +++ b/src/utils/schema-utils.js @@ -84,7 +84,7 @@ export function getTypeInfo(parameter, options = { includeNulls: false, enableEx pattern: (schema.pattern && !schema.enum) ? schema.pattern.replace(/(^\^)|(\$$)/g, '') : '', readOrWriteOnly: schema.readOnly && '🆁' || schema.writeOnly && '🆆' || '', deprecated: !!schema.deprecated, - example: examples || '', + example: examples, default: schema.default ?? '', title: schema.title || '', description: schema.description || '', diff --git a/tests/common-utils.test.js b/tests/common-utils.test.js index 1230cd19..3ea1ddce 100644 --- a/tests/common-utils.test.js +++ b/tests/common-utils.test.js @@ -1,7 +1,7 @@ import test from 'ava'; import { expect } from 'chai'; -import { exampleText, formatExamples } from '../src/utils/common-utils.js'; +import { convertToDisplayString, formatExamples } from '../src/utils/common-utils.js'; test('common-utils.js formatExamples separates multiple examples', t => { expect(formatExamples(['apple', 'banana', 'pear'])).to.equal('apple ┃ banana ┃ pear'); @@ -26,22 +26,22 @@ test('common-utils.js formatExamples gives an empty list no text', t => { t.pass(); }); -test('common-utils.js formatExamples prints one legacy example as text', t => { - expect(formatExamples('apple')).to.equal('apple'); - expect(formatExamples(5)).to.equal('5'); - expect(formatExamples('')).to.equal(''); - expect(formatExamples(undefined)).to.equal(''); - expect(formatExamples({ id: 1 })).to.equal('{"id":1}'); +test('common-utils.js formatExamples prints one legacy example, kept as a one entry list, as text', t => { + expect(formatExamples(['apple'])).to.equal('apple'); + expect(formatExamples([5])).to.equal('5'); + expect(formatExamples([''])).to.equal(''); + expect(formatExamples([{ id: 1 }])).to.equal('{"id":1}'); expect(formatExamples([['a', 'b']])).to.equal('["a","b"]'); t.pass(); }); -test('common-utils.js exampleText uses the first example for a placeholder', t => { - expect(exampleText(['apple', 'banana'])).to.equal('apple'); - expect(exampleText([['a', 'b']])).to.equal('["a","b"]'); - expect(exampleText([0])).to.equal('0'); - expect(exampleText([false])).to.equal('false'); - expect(exampleText([])).to.equal(''); - expect(exampleText(undefined)).to.equal(''); +test('common-utils.js convertToDisplayString keeps text as is and writes any other value as JSON', t => { + expect(convertToDisplayString('apple')).to.equal('apple'); + expect(convertToDisplayString('')).to.equal(''); + expect(convertToDisplayString(0)).to.equal('0'); + expect(convertToDisplayString(false)).to.equal('false'); + expect(convertToDisplayString(null)).to.equal('null'); + expect(convertToDisplayString(['a', 'b'])).to.equal('["a","b"]'); + expect(convertToDisplayString({ id: 1 })).to.equal('{"id":1}'); t.pass(); });