From d75ad1c7148c3db12e3d65c55d41e7c7624e345e Mon Sep 17 00:00:00 2001 From: JulianMaurin Date: Wed, 29 Jul 2026 16:49:26 +0200 Subject: [PATCH] fix(config-tables): expand untitled $ref targets in the value type An option whose schema property is a bare `$ref` to an untitled `$defs` entry rendered an empty value type, since both renderers only read the target's `title`. Expand the target instead, and name the keys of an inline object shape so a union like `batch_size` reads as "integer or `{min, max}`". MRGFY-8297 Change-Id: I25ebe90e607c8227ad921b9fe9d8305801e1ef4f --- src/components/Tables/ConfigOptions.test.tsx | 53 ++++++++++++++++++++ src/components/Tables/ConfigOptions.tsx | 15 ++++++ src/util/schemaToMarkdown.ts | 12 ++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/components/Tables/ConfigOptions.test.tsx b/src/components/Tables/ConfigOptions.test.tsx index 9924ce5179..31c84f694a 100644 --- a/src/components/Tables/ConfigOptions.test.tsx +++ b/src/components/Tables/ConfigOptions.test.tsx @@ -84,6 +84,59 @@ describe('getValueType with x-has-data-type', () => { }); }); +// An untitled $defs entry — pydantic publishes one for a model that overrides +// its JSON schema with a union wrapper (batch_size) — has no label to render, +// so both renderers expand the target instead of emitting an empty type. +const untitledUnionSchema = { + $defs: { + BatchSizeBounds: { + anyOf: [ + { type: 'integer', minimum: 1, maximum: 128 }, + { + type: 'object', + title: 'BatchSizeBounds', + properties: { min: { type: 'integer' }, max: { type: 'integer' } }, + required: ['min', 'max'], + }, + ], + }, + }, +}; + +describe('getValueType with an untitled $ref target', () => { + it('expands the union instead of rendering an empty type', () => { + const html = render(untitledUnionSchema, { $ref: '#/$defs/BatchSizeBounds' }); + expect(html).toContain('integer'); + expect(html).toContain('or'); + expect(html).toContain('{min, max}'); + }); + + it('names the keys of an inline object shape', () => { + const html = render( + {}, + { type: 'object', properties: { min: { type: 'integer' }, max: { type: 'integer' } } } + ); + expect(html).toContain('{min, max}'); + }); + + it('still renders the title of a titled $ref target', () => { + const schema = { $defs: { Commit: { title: 'Commit', type: 'object' } } }; + expect(render(schema, { $ref: '#/$defs/Commit' })).toContain('Commit'); + }); +}); + +describe('getValueTypeText with an untitled $ref target', () => { + it('expands the union instead of emitting an empty cell', () => { + expect( + getValueTypeText(untitledUnionSchema as never, { $ref: '#/$defs/BatchSizeBounds' }) + ).toBe('integer or `{min, max}`'); + }); + + it('does not crash on a dangling $ref', () => { + expect(getValueTypeText({ $defs: {} } as never, { $ref: '#/$defs/Gone' })).toBe(''); + }); +}); + describe('getValueTypeText with x-has-data-type', () => { it('emits a markdown link instead of the enum dump', () => { expect(getValueTypeText({} as never, inlineMarked)).toBe(`[Queue dequeue reason](${HREF})`); diff --git a/src/components/Tables/ConfigOptions.tsx b/src/components/Tables/ConfigOptions.tsx index 9e91dd945a..33c67b0a5a 100644 --- a/src/components/Tables/ConfigOptions.tsx +++ b/src/components/Tables/ConfigOptions.tsx @@ -198,6 +198,15 @@ export function getValueType(schema: object, definition: any): React.ReactElemen valueType = <>list of {typeDescription}; } } else if (definition.$ref !== undefined) { + if (!getTitle(schema, definition.$ref)) { + // An untitled `$defs` entry carries a shape and no label — pydantic + // publishes one whenever a model overrides its JSON schema with a union + // wrapper. Expand the target so the union renders like an inline one, + // instead of showing an empty type. + const target = getItemFromSchema(schema, definition.$ref); + return target ? getValueType(schema, target) : null; + } + const typeLink = getTypeLink(definition.$ref); const typeDescription = (
" so the // templated value type still links to its data-types section. valueType = <>map of {getValueType(schema, definition.additionalProperties)}; + } else if (definition.type === 'object' && definition.properties) { + // An inline object shape has no name to show: list its keys, which is what + // a reader needs to write the value. + valueType = ( + {`{${Object.keys(definition.properties).join(', ')}}`} + ); } else { valueType = {definition.type}; } diff --git a/src/util/schemaToMarkdown.ts b/src/util/schemaToMarkdown.ts index 15d418cb3a..c42e24a796 100644 --- a/src/util/schemaToMarkdown.ts +++ b/src/util/schemaToMarkdown.ts @@ -72,7 +72,13 @@ export function getValueTypeText(schema: Schema, definition: any): string { } if (definition.$ref !== undefined) { - return getTitle(schema, definition.$ref); + const title = getTitle(schema, definition.$ref); + if (title) { + return title; + } + // Untitled `$defs` entry: expand the target rather than emit an empty cell. + const target = getItemFromSchema(schema, definition.$ref); + return target ? getValueTypeText(schema, target) : ''; } if (definition.anyOf || definition.oneOf || definition.allOf) { @@ -92,6 +98,10 @@ export function getValueTypeText(schema: Schema, definition: any): string { return definition.format; } + if (definition.type === 'object' && definition.properties) { + return `\`{${Object.keys(definition.properties).join(', ')}}\``; + } + return definition.type || ''; }