From 3f3bb40ed2b12f140242759f1427bd9c54855bce Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 26 Sep 2025 15:34:06 +0200 Subject: [PATCH 1/4] Highlight discrinator properties in oneOf, allOf, anyOf alternative objects --- .../components/DocumentView/OpenAPI/style.css | 4 + packages/react-openapi/src/OpenAPISchema.tsx | 73 +++++++++++++------ .../react-openapi/src/OpenAPISchemaName.tsx | 14 +++- packages/react-openapi/src/translations/de.ts | 1 + packages/react-openapi/src/translations/en.ts | 1 + packages/react-openapi/src/translations/es.ts | 1 + packages/react-openapi/src/translations/fr.ts | 1 + packages/react-openapi/src/translations/ja.ts | 1 + packages/react-openapi/src/translations/nl.ts | 1 + packages/react-openapi/src/translations/no.ts | 1 + .../react-openapi/src/translations/pt-br.ts | 1 + packages/react-openapi/src/translations/zh.ts | 1 + 12 files changed, 74 insertions(+), 26 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 187f2e6048..d56fe3dc0e 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -207,6 +207,10 @@ @apply line-through opacity-9; } +.openapi-schema-discriminator { + @apply text-primary-subtle/9 text-[0.813rem] lowercase; +} + .openapi-schema-required { @apply text-warning-subtle text-[0.813rem] lowercase; } diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 7c5e71d812..56f55d236b 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -23,6 +23,7 @@ type CircularRefsIds = Map; export interface OpenAPISchemaPropertyEntry { propertyName?: string; required?: boolean | null; + isDiscriminatorProperty?: boolean; schema: OpenAPIV3.SchemaObject; } @@ -69,17 +70,19 @@ function OpenAPISchemaProperty( ); } - if (alternatives) { + if (alternatives?.schemas) { + const { schemas, discriminator } = alternatives; return (
- {alternatives.map((alternativeSchema, index) => ( + {schemas.map((alternativeSchema, index) => (
- {index < alternatives.length - 1 ? ( + {index < schemas.length - 1 ? ( @@ -414,13 +419,19 @@ export function OpenAPISchemaPresentation(props: { /** * Get the sub-properties of a schema. */ -function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] { +function getSchemaProperties( + schema: OpenAPIV3.SchemaObject, + discriminator?: OpenAPIV3.DiscriminatorObject | undefined +): null | OpenAPISchemaPropertyEntry[] { // check array AND schema.items as this is sometimes null despite what the type indicates if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) { const items = schema.items; const itemProperties = getSchemaProperties(items); if (itemProperties) { - return itemProperties; + return itemProperties.map((prop) => ({ + ...prop, + isDiscriminatorProperty: discriminator?.propertyName === prop.propertyName, + })); } // If the items are a primitive type, we don't need to display them @@ -451,6 +462,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche required: Array.isArray(schema.required) ? schema.required.includes(propertyName) : undefined, + isDiscriminatorProperty: discriminator?.propertyName === propertyName, schema: propertySchema, }); }); @@ -471,14 +483,20 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche type AlternativeType = 'oneOf' | 'allOf' | 'anyOf'; +type SchemaAlternatives = { + type: AlternativeType; + schemas: OpenAPIV3.SchemaObject[]; + discriminator?: OpenAPIV3.DiscriminatorObject; +} | null; + /** * Get the alternatives to display for a schema. */ export function getSchemaAlternatives( schema: OpenAPIV3.SchemaObject, ancestors: Set = new Set() -): OpenAPIV3.SchemaObject[] | null { - // Search for alternatives in the items property if it exists +): SchemaAlternatives { + // Check for nested alternatives in `items` if ( schema.items && ('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items) @@ -487,20 +505,21 @@ export function getSchemaAlternatives( } const alternatives: - | [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]] + | [ + AlternativeType, + (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[], + OpenAPIV3.DiscriminatorObject?, + ] | null = (() => { if (schema.anyOf) { - return ['anyOf', schema.anyOf]; + return ['anyOf', schema.anyOf, schema.discriminator]; } - if (schema.oneOf) { - return ['oneOf', schema.oneOf]; + return ['oneOf', schema.oneOf, schema.discriminator]; } - if (schema.allOf) { - return ['allOf', schema.allOf]; + return ['allOf', schema.allOf, schema.discriminator]; } - return null; })(); @@ -508,11 +527,17 @@ export function getSchemaAlternatives( return null; } - const [type, schemas] = alternatives; - return mergeAlternatives( + const [type, schemas, discriminator] = alternatives; + + return { type, - flattenAlternatives(type, schemas, new Set(ancestors).add(schema)) - ); + schemas: + mergeAlternatives( + type, + flattenAlternatives(type, schemas, new Set(ancestors).add(schema)) + ) ?? [], + discriminator, + }; } /** @@ -610,10 +635,10 @@ function flattenAlternatives( } if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) { - const schemas = getSchemaAlternatives(schemaOrRef, ancestors); - if (schemas) { + const alternatives = getSchemaAlternatives(schemaOrRef, ancestors); + if (alternatives?.schemas) { acc.push( - ...schemas.map((schema) => ({ + ...alternatives.schemas.map((schema) => ({ ...schema, required: mergeRequiredFields(schema, latestAncestor), })) diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx index 8994b4d71f..81cc3e71d1 100644 --- a/packages/react-openapi/src/OpenAPISchemaName.tsx +++ b/packages/react-openapi/src/OpenAPISchemaName.tsx @@ -7,6 +7,7 @@ interface OpenAPISchemaNameProps { schema?: OpenAPIV3.SchemaObject; propertyName?: string | React.JSX.Element; required?: boolean | null; + isDiscriminatorProperty?: boolean; type?: string; context: OpenAPIClientContext; } @@ -16,7 +17,7 @@ interface OpenAPISchemaNameProps { * It includes the property name, type, required and deprecated status. */ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { - const { schema, type, propertyName, required, context } = props; + const { schema, type, propertyName, required, isDiscriminatorProperty, context } = props; const additionalItems = schema && getAdditionalItems(schema, context); @@ -27,9 +28,18 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { {propertyName} ) : null} + {isDiscriminatorProperty ? ( + + {t(context.translation, 'discriminator')} + + ) : null} {type || additionalItems ? ( - {type ? {type} : null} + {schema?.const ? ( + const: {schema?.const} + ) : type ? ( + {type} + ) : null} {additionalItems ? ( {additionalItems} ) : null} diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts index 6229acd4fa..5babf5557e 100644 --- a/packages/react-openapi/src/translations/de.ts +++ b/packages/react-openapi/src/translations/de.ts @@ -5,6 +5,7 @@ export const de = { stability_experimental: 'Experimentell', stability_alpha: 'Alpha', stability_beta: 'Beta', + discriminator: 'Diskriminator', copy_to_clipboard: 'In die Zwischenablage kopieren', copied: 'Kopiert', no_content: 'Kein Inhalt', diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts index 3681626898..9c1607c385 100644 --- a/packages/react-openapi/src/translations/en.ts +++ b/packages/react-openapi/src/translations/en.ts @@ -5,6 +5,7 @@ export const en = { stability_experimental: 'Experimental', stability_alpha: 'Alpha', stability_beta: 'Beta', + discriminator: 'Discriminator', copy_to_clipboard: 'Copy to clipboard', copied: 'Copied', no_content: 'No content', diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts index f9e8c58f48..93b8bf2be3 100644 --- a/packages/react-openapi/src/translations/es.ts +++ b/packages/react-openapi/src/translations/es.ts @@ -5,6 +5,7 @@ export const es = { stability_experimental: 'Experimental', stability_alpha: 'Alfa', stability_beta: 'Beta', + discriminator: 'Discriminador', copy_to_clipboard: 'Copiar al portapapeles', copied: 'Copiado', no_content: 'Sin contenido', diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts index fde7a9222c..5f83e3cd12 100644 --- a/packages/react-openapi/src/translations/fr.ts +++ b/packages/react-openapi/src/translations/fr.ts @@ -5,6 +5,7 @@ export const fr = { stability_experimental: 'Expérimental', stability_alpha: 'Alpha', stability_beta: 'Bêta', + discriminator: 'Discriminateur', copy_to_clipboard: 'Copier dans le presse-papiers', copied: 'Copié', no_content: 'Aucun contenu', diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts index 04d43f67ae..73ae37f1f2 100644 --- a/packages/react-openapi/src/translations/ja.ts +++ b/packages/react-openapi/src/translations/ja.ts @@ -5,6 +5,7 @@ export const ja = { stability_experimental: '実験的', stability_alpha: 'アルファ', stability_beta: 'ベータ', + discriminator: '識別子', copy_to_clipboard: 'クリップボードにコピー', copied: 'コピー済み', no_content: 'コンテンツなし', diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts index 2c57d7af4f..f4e62c474a 100644 --- a/packages/react-openapi/src/translations/nl.ts +++ b/packages/react-openapi/src/translations/nl.ts @@ -5,6 +5,7 @@ export const nl = { stability_experimental: 'Experimenteel', stability_alpha: 'Alfa', stability_beta: 'Bèta', + discriminator: 'Discriminator', copy_to_clipboard: 'Kopiëren naar klembord', copied: 'Gekopieerd', no_content: 'Geen inhoud', diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts index 9ef1b80048..abf45c490a 100644 --- a/packages/react-openapi/src/translations/no.ts +++ b/packages/react-openapi/src/translations/no.ts @@ -5,6 +5,7 @@ export const no = { stability_experimental: 'Eksperimentell', stability_alpha: 'Alfa', stability_beta: 'Beta', + discriminator: 'Diskriminator', copy_to_clipboard: 'Kopier til utklippstavle', copied: 'Kopiert', no_content: 'Ingen innhold', diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts index 2e9e7cb2d9..cf27289547 100644 --- a/packages/react-openapi/src/translations/pt-br.ts +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -5,6 +5,7 @@ export const pt_br = { stability_experimental: 'Experimental', stability_alpha: 'Alfa', stability_beta: 'Beta', + discriminator: 'Discriminador', copy_to_clipboard: 'Copiar para a área de transferência', copied: 'Copiado', no_content: 'Sem conteúdo', diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts index f0e81f21bc..de8faa204b 100644 --- a/packages/react-openapi/src/translations/zh.ts +++ b/packages/react-openapi/src/translations/zh.ts @@ -5,6 +5,7 @@ export const zh = { stability_experimental: '实验性', stability_alpha: 'Alpha', stability_beta: 'Beta', + discriminator: '判别器', copy_to_clipboard: '复制到剪贴板', copied: '已复制', no_content: '无内容', From 87638674ec25671cd5a8759c437830881a99099e Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 26 Sep 2025 15:37:05 +0200 Subject: [PATCH 2/4] Add changeset --- .changeset/dirty-mice-sleep.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/dirty-mice-sleep.md diff --git a/.changeset/dirty-mice-sleep.md b/.changeset/dirty-mice-sleep.md new file mode 100644 index 0000000000..4071c03a07 --- /dev/null +++ b/.changeset/dirty-mice-sleep.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Highlight discriminator properties in oneOf, allOf, anyOf objects From 355ebc6024be8bc09ae399a4579e74b768917e4e Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 26 Sep 2025 15:55:20 +0200 Subject: [PATCH 3/4] Fix TS, tests --- .../react-openapi/src/OpenAPISchema.test.ts | 129 ++++++++++-------- 1 file changed, 72 insertions(+), 57 deletions(-) diff --git a/packages/react-openapi/src/OpenAPISchema.test.ts b/packages/react-openapi/src/OpenAPISchema.test.ts index 4d2adcfc09..22e0b414c8 100644 --- a/packages/react-openapi/src/OpenAPISchema.test.ts +++ b/packages/react-openapi/src/OpenAPISchema.test.ts @@ -22,17 +22,20 @@ describe('getSchemaAlternatives', () => { }, ], }) - ).toEqual([ - { - type: 'number', - }, - { - type: 'boolean', - }, - { - type: 'string', - }, - ]); + ).toEqual({ + type: 'oneOf', + schemas: [ + { + type: 'number', + }, + { + type: 'boolean', + }, + { + type: 'string', + }, + ], + }); }); it('merges string enum', () => { @@ -54,13 +57,16 @@ describe('getSchemaAlternatives', () => { }, ], }) - ).toEqual([ - { - type: 'string', - enum: ['a', 'b', 'c', 'd'], - nullable: true, - }, - ]); + ).toEqual({ + type: 'oneOf', + schemas: [ + { + type: 'string', + enum: ['a', 'b', 'c', 'd'], + nullable: true, + }, + ], + }); }); it('merges objects with allOf', () => { @@ -93,26 +99,29 @@ describe('getSchemaAlternatives', () => { }, ], }) - ).toEqual([ - { - type: 'object', - properties: { - name: { - type: 'string', - }, - map: { - type: 'string', - }, - description: { - type: 'string', - }, - externalId: { - type: 'string', + ).toEqual({ + type: 'allOf', + schemas: [ + { + type: 'object', + properties: { + name: { + type: 'string', + }, + map: { + type: 'string', + }, + description: { + type: 'string', + }, + externalId: { + type: 'string', + }, }, + required: ['name', 'map', 'externalId'], }, - required: ['name', 'map', 'externalId'], - }, - ]); + ], + }); }); it('should not flatten oneOf and allOf', () => { @@ -134,21 +143,24 @@ describe('getSchemaAlternatives', () => { }, ], }) - ).toEqual([ - { - allOf: [ - { - type: 'number', - }, - { - type: 'boolean', - }, - ], - }, - { - type: 'string', - }, - ]); + ).toEqual({ + type: 'oneOf', + schemas: [ + { + allOf: [ + { + type: 'number', + }, + { + type: 'boolean', + }, + ], + }, + { + type: 'string', + }, + ], + }); }); it('should stop at circular references', () => { @@ -162,11 +174,14 @@ describe('getSchemaAlternatives', () => { a.anyOf?.push(a); - expect(getSchemaAlternatives(a)).toEqual([ - { - type: 'string', - }, - a, - ]); + expect(getSchemaAlternatives(a)).toEqual({ + type: 'anyOf', + schemas: [ + { + type: 'string', + }, + a, + ], + }); }); }); From 031f3d67f04cab0afecfb1cf42e211b8da5b7a7a Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Mon, 29 Sep 2025 16:42:35 +0200 Subject: [PATCH 4/4] review: fix alignment --- packages/gitbook/src/components/DocumentView/OpenAPI/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index d56fe3dc0e..2642f2fb1f 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -192,7 +192,7 @@ .openapi-schema-name { /* To make double click on the property name select only the name, we disable selection on the parent and re-enable it on the children. */ - @apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5; + @apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5 items-center; } .openapi-schema-name .openapi-deprecated {