From ae108e9c7666bc4e7e96434a7214afebd9c8d5ee Mon Sep 17 00:00:00 2001 From: Carsten Kirschner Date: Mon, 9 Nov 2026 12:31:56 +0100 Subject: [PATCH 1/3] Render patternProperties of objects in multi-type properties and of untyped oneOf, anyOf and allOf options. Co-Authored-By: Claude Opus 5.5 --- mocks/multi-type-variants-schema.json | 175 ++++++++++++++++++++++++++ mocks/multi-type-variants.html | 11 ++ src/utils/schema-utils.js | 9 +- tests/schema-utils.test.js | 29 ++++- 4 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 mocks/multi-type-variants-schema.json create mode 100644 mocks/multi-type-variants.html diff --git a/mocks/multi-type-variants-schema.json b/mocks/multi-type-variants-schema.json new file mode 100644 index 00000000..fd068c74 --- /dev/null +++ b/mocks/multi-type-variants-schema.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Multi-type and xxx-of variants", + "version": "1.0", + "description": "Every way a `patternProperties` object can appear as a child: multi-type (`type: [object, null]`), oneOf, anyOf, allOf, typed and untyped. Each must show the `` key with the Translation fields." + }, + "paths": { + "/pattern-properties": { + "post": { + "summary": "patternProperties in child relations", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "typeArrayObject": { + "type": [ + "object", + "null" + ], + "description": "type: [object, null] with patternProperties.", + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + }, + "arrayOfNullableObjects": { + "type": "array", + "description": "Array whose items are type: [object, null] with patternProperties.", + "items": { + "type": [ + "object", + "null" + ], + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + } + }, + "mapOfNullableObjects": { + "type": "object", + "description": "additionalProperties of type: [object, null] with patternProperties.", + "additionalProperties": { + "type": [ + "object", + "null" + ], + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + } + }, + "oneOfTyped": { + "description": "oneOf, option has type: object (worked before).", + "oneOf": [ + { + "type": "object", + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + }, + { + "type": "null" + } + ] + }, + "anyOfTyped": { + "description": "anyOf, option has type: object (worked before).", + "anyOf": [ + { + "type": "object", + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + }, + { + "type": "string" + } + ] + }, + "allOfTyped": { + "description": "allOf, member has type: object (worked before).", + "allOf": [ + { + "type": "object", + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + } + ] + }, + "oneOfUntyped": { + "description": "oneOf, option has only patternProperties.", + "oneOf": [ + { + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + }, + { + "type": "null" + } + ] + }, + "anyOfUntyped": { + "description": "anyOf, option has only patternProperties.", + "anyOf": [ + { + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + }, + { + "type": "string" + } + ] + }, + "allOfUntyped": { + "description": "allOf, member has only patternProperties.", + "allOf": [ + { + "patternProperties": { + "[a-z]{2}": { + "$ref": "#/components/schemas/Translation" + } + } + } + ] + } + } + } + } + } + }, + "responses": { + "204": { + "description": "No content" + } + } + } + } + }, + "components": { + "schemas": { + "Translation": { + "type": "object", + "properties": { + "subject": { + "type": "string" + }, + "body": { + "type": "string" + } + } + } + } + } +} diff --git a/mocks/multi-type-variants.html b/mocks/multi-type-variants.html new file mode 100644 index 00000000..94f5296d --- /dev/null +++ b/mocks/multi-type-variants.html @@ -0,0 +1,11 @@ + + + + + + Multi-type and xxx-of variants + + + + + diff --git a/src/utils/schema-utils.js b/src/utils/schema-utils.js index a1ebb4e4..a4cd21da 100644 --- a/src/utils/schema-utils.js +++ b/src/utils/schema-utils.js @@ -329,7 +329,7 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' // If allOf is an array of multiple elements, then all the keys makes a single object const objWithAllProps = {}; allOf.map((v, i) => { - if (v.type === 'object' || v.properties || v.allOf || v.anyOf || v.oneOf) { + if (v.type === 'object' || v.properties || v.patternProperties || v.allOf || v.anyOf || v.oneOf) { const propSuffix = (v.anyOf || v.oneOf) && i > 0 ? i : ''; const partialObj = schemaInObjectNotation(v, options, (level + 1), propSuffix); Object.assign(objWithAllProps, partialObj); @@ -358,7 +358,7 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' let readOnly = true; (anyOf || oneOf || []).forEach((v, index) => { - if (v.type === 'object' || v.properties || v.allOf || v.anyOf || v.oneOf || v.type === 'array' || v.items) { + if (v.type === 'object' || v.properties || v.patternProperties || v.allOf || v.anyOf || v.oneOf || v.type === 'array' || v.items) { const partialObj = schemaInObjectNotation(v, options); if (partialObj) { objWithAnyOfProps[`::OPTION~${index + 1}${v.title ? `~${v.title}` : ''}`] = partialObj; @@ -455,6 +455,9 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' objTypeOption[key] = schemaInObjectNotation(schemaProperties[key], options, (level + 1)); } } + for (const key in schemaPatternProperties) { + objTypeOption[``] = schemaInObjectNotation(schemaPatternProperties[key], options, (level + 1)); + } if (schema.additionalProperties) { objTypeOption[''] = schemaInObjectNotation(schema.additionalProperties, options); @@ -481,7 +484,7 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' return obj; } - if (propertyType === 'object' || schemaProperties || schema.circularReference) { + if (propertyType === 'object' || schemaProperties || schemaPatternProperties || schema.circularReference) { const obj = { '::type': '' }; obj['::title'] = schema.title || ''; obj['::description'] = schema.description || ''; diff --git a/tests/schema-utils.test.js b/tests/schema-utils.test.js index 9e6e41ff..00a23849 100644 --- a/tests/schema-utils.test.js +++ b/tests/schema-utils.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { expect } from 'chai'; import xmlFormatter from '../src/utils/xml/xml.js'; -import { getExampleValuesFromSchema } from '../src/utils/schema-utils.js'; +import { getExampleValuesFromSchema, schemaInObjectNotation } from '../src/utils/schema-utils.js'; test('schema-utils.js getExampleValuesFromSchema', t => { const schema = { @@ -70,3 +70,30 @@ test('schema-utils.js getExampleValuesFromSchema', t => { expect(formattedResult).to.eql(expectedFormattedResult); t.pass(); }); + +const patternProperties = { '[a-z]{2}': { type: 'object', properties: { name: { type: 'string' } } } }; +const patternKey = ''; +const findPatternOwner = node => { + if (!node || typeof node !== 'object') { return undefined; } + if (node[patternKey]) { return node; } + return Object.values(node).map(findPatternOwner).find(n => n); +}; +[ + ['a multi-type object', { type: ['object', 'null'], patternProperties }], + ['array items of a multi-type object', { type: 'array', items: { type: ['object', 'null'], patternProperties } }], + ['additionalProperties of a multi-type object', { type: 'object', additionalProperties: { type: ['object', 'null'], patternProperties } }], + ['a typed one-of option', { oneOf: [{ type: 'object', patternProperties }, { type: 'null' }] }], + ['a typed any-of option', { anyOf: [{ type: 'object', patternProperties }, { type: 'string' }] }], + ['an untyped one-of option', { oneOf: [{ patternProperties }, { type: 'null' }] }], + ['an untyped any-of option', { anyOf: [{ patternProperties }, { type: 'string' }] }], + ['a typed all-of member', { allOf: [{ type: 'object', patternProperties }] }], + ['an untyped all-of member', { allOf: [{ patternProperties }] }], + ['an untyped object', { patternProperties }] +].forEach(([name, schema]) => { + test(`schema-utils.js schemaInObjectNotation renders patternProperties of ${name}`, t => { + const owner = findPatternOwner(schemaInObjectNotation(schema, {})); + expect(owner, 'pattern key rendered').to.not.equal(undefined); + expect(owner[patternKey]).to.have.property('name'); + t.pass(); + }); +}); From d2a66147bb84618b9b3f37d3bb5256a74fd1f764 Mon Sep 17 00:00:00 2001 From: Carsten Kirschner Date: Mon, 9 Nov 2026 13:38:57 +0100 Subject: [PATCH 2/3] Remove Mock and extra test --- mocks/multi-type-variants-schema.json | 175 -------------------------- mocks/multi-type-variants.html | 11 -- tests/schema-utils.test.js | 29 +---- 3 files changed, 1 insertion(+), 214 deletions(-) delete mode 100644 mocks/multi-type-variants-schema.json delete mode 100644 mocks/multi-type-variants.html diff --git a/mocks/multi-type-variants-schema.json b/mocks/multi-type-variants-schema.json deleted file mode 100644 index fd068c74..00000000 --- a/mocks/multi-type-variants-schema.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "Multi-type and xxx-of variants", - "version": "1.0", - "description": "Every way a `patternProperties` object can appear as a child: multi-type (`type: [object, null]`), oneOf, anyOf, allOf, typed and untyped. Each must show the `` key with the Translation fields." - }, - "paths": { - "/pattern-properties": { - "post": { - "summary": "patternProperties in child relations", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "typeArrayObject": { - "type": [ - "object", - "null" - ], - "description": "type: [object, null] with patternProperties.", - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - }, - "arrayOfNullableObjects": { - "type": "array", - "description": "Array whose items are type: [object, null] with patternProperties.", - "items": { - "type": [ - "object", - "null" - ], - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - } - }, - "mapOfNullableObjects": { - "type": "object", - "description": "additionalProperties of type: [object, null] with patternProperties.", - "additionalProperties": { - "type": [ - "object", - "null" - ], - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - } - }, - "oneOfTyped": { - "description": "oneOf, option has type: object (worked before).", - "oneOf": [ - { - "type": "object", - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - }, - { - "type": "null" - } - ] - }, - "anyOfTyped": { - "description": "anyOf, option has type: object (worked before).", - "anyOf": [ - { - "type": "object", - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - }, - { - "type": "string" - } - ] - }, - "allOfTyped": { - "description": "allOf, member has type: object (worked before).", - "allOf": [ - { - "type": "object", - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - } - ] - }, - "oneOfUntyped": { - "description": "oneOf, option has only patternProperties.", - "oneOf": [ - { - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - }, - { - "type": "null" - } - ] - }, - "anyOfUntyped": { - "description": "anyOf, option has only patternProperties.", - "anyOf": [ - { - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - }, - { - "type": "string" - } - ] - }, - "allOfUntyped": { - "description": "allOf, member has only patternProperties.", - "allOf": [ - { - "patternProperties": { - "[a-z]{2}": { - "$ref": "#/components/schemas/Translation" - } - } - } - ] - } - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - } - } - } - } - }, - "components": { - "schemas": { - "Translation": { - "type": "object", - "properties": { - "subject": { - "type": "string" - }, - "body": { - "type": "string" - } - } - } - } - } -} diff --git a/mocks/multi-type-variants.html b/mocks/multi-type-variants.html deleted file mode 100644 index 94f5296d..00000000 --- a/mocks/multi-type-variants.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Multi-type and xxx-of variants - - - - - diff --git a/tests/schema-utils.test.js b/tests/schema-utils.test.js index 00a23849..9e6e41ff 100644 --- a/tests/schema-utils.test.js +++ b/tests/schema-utils.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { expect } from 'chai'; import xmlFormatter from '../src/utils/xml/xml.js'; -import { getExampleValuesFromSchema, schemaInObjectNotation } from '../src/utils/schema-utils.js'; +import { getExampleValuesFromSchema } from '../src/utils/schema-utils.js'; test('schema-utils.js getExampleValuesFromSchema', t => { const schema = { @@ -70,30 +70,3 @@ test('schema-utils.js getExampleValuesFromSchema', t => { expect(formattedResult).to.eql(expectedFormattedResult); t.pass(); }); - -const patternProperties = { '[a-z]{2}': { type: 'object', properties: { name: { type: 'string' } } } }; -const patternKey = ''; -const findPatternOwner = node => { - if (!node || typeof node !== 'object') { return undefined; } - if (node[patternKey]) { return node; } - return Object.values(node).map(findPatternOwner).find(n => n); -}; -[ - ['a multi-type object', { type: ['object', 'null'], patternProperties }], - ['array items of a multi-type object', { type: 'array', items: { type: ['object', 'null'], patternProperties } }], - ['additionalProperties of a multi-type object', { type: 'object', additionalProperties: { type: ['object', 'null'], patternProperties } }], - ['a typed one-of option', { oneOf: [{ type: 'object', patternProperties }, { type: 'null' }] }], - ['a typed any-of option', { anyOf: [{ type: 'object', patternProperties }, { type: 'string' }] }], - ['an untyped one-of option', { oneOf: [{ patternProperties }, { type: 'null' }] }], - ['an untyped any-of option', { anyOf: [{ patternProperties }, { type: 'string' }] }], - ['a typed all-of member', { allOf: [{ type: 'object', patternProperties }] }], - ['an untyped all-of member', { allOf: [{ patternProperties }] }], - ['an untyped object', { patternProperties }] -].forEach(([name, schema]) => { - test(`schema-utils.js schemaInObjectNotation renders patternProperties of ${name}`, t => { - const owner = findPatternOwner(schemaInObjectNotation(schema, {})); - expect(owner, 'pattern key rendered').to.not.equal(undefined); - expect(owner[patternKey]).to.have.property('name'); - t.pass(); - }); -}); From db8677ad90e6e883022c9ef611dfc6a4262e8cfd Mon Sep 17 00:00:00 2001 From: Carsten Kirschner Date: Mon, 9 Nov 2026 14:22:03 +0100 Subject: [PATCH 3/3] Tests can stay --- tests/schema-utils.test.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/schema-utils.test.js b/tests/schema-utils.test.js index 9e6e41ff..00a23849 100644 --- a/tests/schema-utils.test.js +++ b/tests/schema-utils.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { expect } from 'chai'; import xmlFormatter from '../src/utils/xml/xml.js'; -import { getExampleValuesFromSchema } from '../src/utils/schema-utils.js'; +import { getExampleValuesFromSchema, schemaInObjectNotation } from '../src/utils/schema-utils.js'; test('schema-utils.js getExampleValuesFromSchema', t => { const schema = { @@ -70,3 +70,30 @@ test('schema-utils.js getExampleValuesFromSchema', t => { expect(formattedResult).to.eql(expectedFormattedResult); t.pass(); }); + +const patternProperties = { '[a-z]{2}': { type: 'object', properties: { name: { type: 'string' } } } }; +const patternKey = ''; +const findPatternOwner = node => { + if (!node || typeof node !== 'object') { return undefined; } + if (node[patternKey]) { return node; } + return Object.values(node).map(findPatternOwner).find(n => n); +}; +[ + ['a multi-type object', { type: ['object', 'null'], patternProperties }], + ['array items of a multi-type object', { type: 'array', items: { type: ['object', 'null'], patternProperties } }], + ['additionalProperties of a multi-type object', { type: 'object', additionalProperties: { type: ['object', 'null'], patternProperties } }], + ['a typed one-of option', { oneOf: [{ type: 'object', patternProperties }, { type: 'null' }] }], + ['a typed any-of option', { anyOf: [{ type: 'object', patternProperties }, { type: 'string' }] }], + ['an untyped one-of option', { oneOf: [{ patternProperties }, { type: 'null' }] }], + ['an untyped any-of option', { anyOf: [{ patternProperties }, { type: 'string' }] }], + ['a typed all-of member', { allOf: [{ type: 'object', patternProperties }] }], + ['an untyped all-of member', { allOf: [{ patternProperties }] }], + ['an untyped object', { patternProperties }] +].forEach(([name, schema]) => { + test(`schema-utils.js schemaInObjectNotation renders patternProperties of ${name}`, t => { + const owner = findPatternOwner(schemaInObjectNotation(schema, {})); + expect(owner, 'pattern key rendered').to.not.equal(undefined); + expect(owner[patternKey]).to.have.property('name'); + t.pass(); + }); +});