Repository: scalar/scalar
Pull request #5723: feat(api-reference): discriminator support
Author: login: antlio
Created: 2025-05-23T09:38:11Z | Merged: 2025-05-23T20:47:36Z

**Problem**

currently in discriminator presence reference is not retrieving mapping nor resolving schemas in its ui.

**Solution**

this pr introduces schema discriminator in order to be able to select schema accordingly and fixes #795 .

| after |
|------|
| <img width="1464" alt="image" src="https://github.com/user-attachments/assets/b31d894d-f905-446f-9555-35b2f1d49f0c" /> |

**Test**
[Open API spec doc](https://sandbox.scalar.com/files/FRNqy/openapi.yaml)

**Checklist**

I’ve gone through the following:

- [x] I’ve added an explanation _why_ this change is needed.
- [x] I’ve added a changeset (`pnpm changeset`).
- [x] I’ve added tests for the regression or new feature.
- [ ] I’ve updated the documentation.

<!-- See the [contributing guide](https://github.com/scalar/scalar/blob/main/CONTRIBUTING.md) for more information. -->


Linked issue #795: Options could be clearer when using a discriminator
### Expected Behavior

When using a [discriminator](https://spec.openapis.org/oas/v3.0.3#discriminator-object) a user should be able to tell which of the  oneOf/AnyOf/allOf options are which, and what property should be used to discriminate between them.

### Actual Behavior

Currently each of the available options is deserialised and listed in full and each section doesn't have a title, so while the options are different the user might not know which one they should be choosing. The name of the discriminator is also not displayed anywhere.

### Reproduction steps

```bash
1. Create a spec such as:

  
  {
    [...]
    "paths": {
      "/foo": {
        "put": {
          [...]
          "requestBody": {
            "content": {
              "application/json": {
                "schema": {
                  [...]
                  "discriminator": {
                    "propertyName": "type",
                    "mapping": {
                      "foo": "#/components/schemas/foo",
                      "bar": "#/components/schemas/bar"
                    },
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/foo"
                      },
                      {
                        "$ref": "#/components/schemas/bar"
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "components": {
      "schemas": {
        "foo": {
          [Some fields go here...]
        },
        "bar": {
          [Some fields go here...]
        }
      }
    }
  }  
  ```
2. Each of the fields within `#/components/schemas/foo` and `#/components/schemas/bar` will be listed but it won't show which is `foo` and which is `bar`.
3. The propertyName `type` will also not be diaplyed
```


### Link to site / OpenAPI Spec

_No response_

### Version

1.13.4

### What browsers are you seeing the problem on?

Chrome
Linked issue comments: user: login: hanspagel
created_at: 2024-01-16T14:16:46Z
body: Thanks for reporting! I thought this is rendered properly, will check.
author_association: MEMBER
user: login: hanspagel
created_at: 2024-01-19T12:02:09Z
body: Ah, it’s something else. I’ve just learned about the discriminator. I’m leaving this example spec here:

https://sandbox.scalar.com/e/fUEFn
author_association: MEMBER
user: login: tobystokes
created_at: 2024-04-10T10:53:48Z
body: Same. if you want another real-life example, see https://hazy.apidocumentation.com/api-reference#tag/jobs/post/api/jobs/train/

Under the training_cfg.model. model_output there are 3 `anyof` possibles that would be clearer if the `title` attribute was rendered.

![image](https://github.com/scalar/scalar/assets/109024/7acf41a0-535f-4c31-8b8e-890a4f339c6e)

author_association: NONE
user: login: cpbr-dev
created_at: 2024-04-11T15:02:04Z
body: Adding to this as I've just stumbled upon this issue within the documentation I'm working on.
Here is how Redoc handles it https://redocly.com/docs/resources/discriminator/

![image](https://github.com/scalar/scalar/assets/30962666/77f25ab3-5403-4e08-b624-b54943a4a259)

Selecting one of the options will change the rendering to show only its parameters:

![image](https://github.com/scalar/scalar/assets/30962666/cdaeeef0-e888-41d5-ba4c-f009c2ddd5b2)
![image](https://github.com/scalar/scalar/assets/30962666/96bf713f-7c9b-4a90-aa6e-c6aa7f85e275)


Currently Scalar doesn't display it in an intuitive way, the test request panel also seems to default to the first schema in the list with no option to pick a different one unless you type it out yourself.

author_association: NONE
user: login: ivansenic
created_at: 2024-05-20T09:22:47Z
body: Upvoting this one, as we are hitting the same issue.
author_association: NONE
user: login: kristianmitk
created_at: 2024-05-29T13:51:24Z
body: Also expressing the interest for seeing scalar supporting the `discriminator` field. 

It is insofar important, as otherwise any endpoint relying on polymorphic structures is basically incompletely rendered, making it unfortunately unusable for clients. 
Observed it in various places, that OpenAPI tools - be it code generators or reference UIs - are lacking any/proper support for it. Not something that's specific to this project. But maybe something that could make it stand out.

For the time being, does anybody have any suggestions how to circumvent this limitation except fundamental changes in API design like breaking up polymorphic structures into multiple endpoints?
 
author_association: NONE
user: login: wiledal
created_at: 2025-02-11T09:45:16Z
body: Hello! 👋 

This is still an issue, and makes legibility quite difficult for polymorphic types.

Example 👇 
<img src="https://github.com/user-attachments/assets/119a3a98-bcfe-4a33-89c9-7666d8390589" width="400">

Are there any ideas for improvements on the horizon?
author_association: NONE
user: login: LeaFrock
created_at: 2025-04-03T03:31:06Z
body: The same issue +1.
author_association: NONE
user: login: agiannone
created_at: 2025-04-04T08:10:06Z
body: Facing the same issue +1
author_association: NONE

Diff:
diff --git a/.changeset/eight-gorillas-learn.md b/.changeset/eight-gorillas-learn.md
new file mode 100644
index 00000000000..844b403c315
--- /dev/null
+++ b/.changeset/eight-gorillas-learn.md
@@ -0,0 +1,5 @@
+---
+'@scalar/api-reference': patch
+---
+
+feat: adds schema discriminator helper functions
diff --git a/.changeset/neat-lions-check.md b/.changeset/neat-lions-check.md
new file mode 100644
index 00000000000..409fbfc315f
--- /dev/null
+++ b/.changeset/neat-lions-check.md
@@ -0,0 +1,5 @@
+---
+'@scalar/api-reference': patch
+---
+
+feat: adds schema discriminator support
diff --git a/packages/api-reference/src/components/Content/Schema/Schema.vue b/packages/api-reference/src/components/Content/Schema/Schema.vue
index ba3d4dc1091..366d8f68a0b 100644
--- a/packages/api-reference/src/components/Content/Schema/Schema.vue
+++ b/packages/api-reference/src/components/Content/Schema/Schema.vue
@@ -2,8 +2,14 @@
 import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
 import { ScalarIcon, ScalarMarkdown } from '@scalar/components'
 import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types'
-import { computed } from 'vue'
+import { computed, ref, watch } from 'vue'
 
+import {
+  getDiscriminatorMapping,
+  getDiscriminatorPropertyName,
+  mergeDiscriminatorSchemas,
+} from '@/components/Content/Schema/helpers/schema-discriminator'
+import SchemaDiscriminator from '@/components/Content/Schema/SchemaDiscriminator.vue'
 import ScreenReader from '@/components/ScreenReader.vue'
 
 import SchemaHeading from './SchemaHeading.vue'
@@ -36,9 +42,58 @@ const props = withDefaults(
       | Record<string, OpenAPIV3_1.SchemaObject>
       | unknown
   }>(),
-  { level: 0, showAdditionalProperties: false, noncollapsible: false },
+  { level: 0, noncollapsible: false },
 )
 
+const selectedDiscriminatorType = ref<string>('')
+
+/** Returns the discriminator mapping */
+const discriminatorMapping = computed(() => {
+  if (!props.value) {
+    return undefined
+  }
+
+  return getDiscriminatorMapping(props.value)
+})
+
+const defaultDiscriminatorType = computed(() => {
+  if (!discriminatorMapping.value) {
+    return ''
+  }
+
+  return Object.keys(discriminatorMapping.value)[0] || ''
+})
+
+/** Returns the name of the discriminator property */
+const discriminatorPropertyName = computed(() => {
+  if (!props.value) {
+    return undefined
+  }
+
+  return getDiscriminatorPropertyName(props.value)
+})
+
+/** Returns merged schema */
+const resolvedSchema = computed(() => {
+  if (!props.value) {
+    return undefined
+  }
+
+  // If no discriminator or no selected type, return original value
+  if (!discriminatorMapping.value || !selectedDiscriminatorType.value) {
+    return props.value
+  }
+
+  // Merge schemas based on discriminator
+  return (
+    mergeDiscriminatorSchemas(
+      props.value as OpenAPIV3_1.SchemaObject,
+      selectedDiscriminatorType.value,
+      props.schemas as Record<string, OpenAPIV3_1.SchemaObject>,
+    ) || props.value
+  )
+})
+
 const shouldShowToggle = computed(() => {
   if (props.noncollapsible || props.level === 0) {
     return false
@@ -50,6 +105,17 @@ const shouldShowToggle = computed(() => {
 // Prevent click action if noncollapsible
 const handleClick = (e: MouseEvent) =>
   props.noncollapsible && e.stopPropagation()
+
+// Watch for changes in discriminator mapping and update selected type if needed
+watch(
+  discriminatorMapping,
+  (newMapping) => {
+    if (newMapping && !selectedDiscriminatorType.value) {
+      selectedDiscriminatorType.value = defaultDiscriminatorType.value
+    }
+  },
+  { immediate: true },
+)
 </script>
 <template>
   <Disclosure
@@ -63,18 +129,19 @@ const handleClick = (e: MouseEvent) =>
         { 'schema-card--compact': compact, 'schema-card--open': open },
         { 'border-t-1/2': additionalProperties && open },
       ]">
+      <!-- Schema description -->
       <div
         v-if="
-          value?.description &&
-          typeof value.description === 'string' &&
-          !value.allOf &&
-          !value.oneOf &&
-          !value.anyOf &&
+          resolvedSchema?.description &&
+          typeof resolvedSchema.description === 'string' &&
+          !resolvedSchema.allOf &&
+          !resolvedSchema.oneOf &&
+          !resolvedSchema.anyOf &&
           !compact
         "
         class="schema-card-description">
-        <template v-if="!value.enum">
-          <ScalarMarkdown :value="value.description" />
+        <template v-if="!resolvedSchema.enum">
+          <ScalarMarkdown :value="resolvedSchema.description" />
         </template>
       </div>
       <div
@@ -138,30 +205,48 @@ const handleClick = (e: MouseEvent) =>
         <DisclosurePanel
           as="ul"
           :static="!shouldShowToggle">
+          <!-- Discriminator selector -->
+          <SchemaDiscriminator
+            v-if="discriminatorMapping && resolvedSchema?.properties"
+            v-model="selectedDiscriminatorType"
+            :discriminatorMapping="discriminatorMapping"
+            :discriminatorPropertyName="discriminatorPropertyName" />
+
+          <!-- Schema properties -->
           <template
             v-if="
-              value.properties ||
-              value.additionalProperties ||
-              value.patternProperties
+              resolvedSchema &&
+              ('properties' in resolvedSchema ||
+                'additionalProperties' in resolvedSchema ||
+                'patternProperties' in resolvedSchema)
             ">
-            <template v-if="value.properties">
+            <!-- Regular properties -->
+            <template v-if="resolvedSchema.properties">
               <SchemaProperty
-                v-for="property in Object.keys(value?.properties)"
+                v-for="property in Object.keys(resolvedSchema.properties)"
                 :key="property"
                 :compact="compact"
                 :hideHeading="hideHeading"
                 :level="level + 1"
                 :name="property"
                 :required="
-                  value.required?.includes(property) ||
-                  value.properties?.[property]?.required === true
+                  resolvedSchema.required?.includes(property) ||
+                  resolvedSchema.properties[property]?.required === true
                 "
                 :schemas="schemas"
-                :value="value.properties?.[property]" />
+                :value="{
+                  ...resolvedSchema.properties[property],
+                  parent: resolvedSchema,
+                  isDiscriminator: property === discriminatorPropertyName,
+                }" />
             </template>
-            <template v-if="value.patternProperties">
+
+            <!-- Pattern properties -->
+            <template v-if="resolvedSchema.patternProperties">
               <SchemaProperty
-                v-for="property in Object.keys(value?.patternProperties)"
+                v-for="property in Object.keys(
+                  resolvedSchema.patternProperties,
+                )"
                 :key="property"
                 :compact="compact"
                 :hideHeading="hideHeading"
@@ -169,18 +254,25 @@ const handleClick = (e: MouseEvent) =>
                 :name="property"
                 pattern
                 :schemas="schemas"
-                :value="value.patternProperties?.[property]" />
+                :value="
+                  value.discriminator?.propertyName === property
+                    ? value
+                    : resolvedSchema.patternProperties[property]
+                " />
             </template>
-            <template v-if="value.additionalProperties">
+
+            <!-- Additional properties -->
+            <template v-if="resolvedSchema.additionalProperties">
               <!--
                 Allows any type of additional property value
                 @see https://swagger.io/docs/specification/data-models/dictionaries/#free-form
                -->
               <SchemaProperty
                 v-if="
-                  value.additionalProperties === true ||
-                  Object.keys(value.additionalProperties).length === 0 ||
-                  !value.additionalProperties.type
+                  resolvedSchema.additionalProperties === true ||
+                  Object.keys(resolvedSchema.additionalProperties).length ===
+                    0 ||
+                  !('type' in resolvedSchema.additionalProperties)
                 "
                 additional
                 :compact="compact"
@@ -190,11 +282,10 @@ const handleClick = (e: MouseEvent) =>
                 :schemas="schemas"
                 :value="{
                   type: 'anything',
-                  ...(typeof value.additionalProperties === 'object'
-                    ? value.additionalProperties
+                  ...(typeof resolvedSchema.additionalProperties === 'object'
+                    ? resolvedSchema.additionalProperties
                     : {}),
                 }" />
-              <!-- Allows a specific type of additional property value -->
               <SchemaProperty
                 v-else
                 additional
@@ -203,17 +294,27 @@ const handleClick = (e: MouseEvent) =>
                 :level="level"
                 noncollapsible
                 :schemas="schemas"
-                :value="value.additionalProperties" />
+                :value="
+                  value.discriminator?.propertyName === name
+                    ? value
+                    : resolvedSchema.additionalProperties
+                " />
             </template>
           </template>
+
+          <!-- Single property -->
           <template v-else>
             <SchemaProperty
               :compact="compact"
               :hideHeading="hideHeading"
-              :level="level"
-              :name="(value as OpenAPIV2.SchemaObject).name"
+              :level="level + 1"
+              :name="(resolvedSchema as OpenAPIV3_1.SchemaObject).name"
               :schemas="schemas"
-              :value="value" />
+              :value="
+                value.discriminator?.propertyName === name
+                  ? value
+                  : resolvedSchema
+              " />
           </template>
         </DisclosurePanel>
       </div>
@@ -277,6 +378,7 @@ button.schema-card-title:hover {
 
   border: var(--scalar-border-width) solid var(--scalar-border-color);
   border-radius: var(--scalar-radius-lg);
+  overflow: hidden;
   width: fit-content;
 }
 .schema-properties-name {
@@ -299,7 +401,7 @@ button.schema-card-title:hover {
 }
 .schema-card-title--compact {
   color: var(--scalar-color-2);
-  padding: 6px 8px;
+  padding: 6px;
   height: auto;
   border-bottom: none;
 }
diff --git a/packages/api-reference/src/components/Content/Schema/SchemaDiscriminator.vue b/packages/api-reference/src/components/Content/Schema/SchemaDiscriminator.vue
new file mode 100644
index 00000000000..6ca70c857fe
--- /dev/null
+++ b/packages/api-reference/src/components/Content/Schema/SchemaDiscriminator.vue
@@ -0,0 +1,53 @@
+<script lang="ts" setup>
+import { ScalarListbox, type ScalarListboxOption } from '@scalar/components'
+import { ScalarIconCaretDown } from '@scalar/icons'
+import { computed } from 'vue'
+
+import type {
+  DiscriminatorMapping,
+  DiscriminatorPropertyName,
+} from './helpers/schema-discriminator'
+
+const { discriminatorMapping, modelValue, discriminatorPropertyName } =
+  defineProps<{
+    discriminatorMapping: DiscriminatorMapping
+    modelValue: string
+    discriminatorPropertyName?: DiscriminatorPropertyName
+  }>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void
+}>()
+
+const listboxOptions = computed(() =>
+  Object.keys(discriminatorMapping).map((type) => ({
+    id: type,
+    label: type,
+  })),
+)
+
+const selectedOption = computed({
+  get: () =>
+    listboxOptions.value.find(
+      (opt: ScalarListboxOption) => opt.id === modelValue,
+    ),
+  set: (opt: ScalarListboxOption) => emit('update:modelValue', opt.id),
+})
+</script>
+
+<template>
+  <ScalarListbox
+    v-model="selectedOption"
+    :options="listboxOptions"
+    resize>
+    <button
+      class="bg-b-1.5 hover:bg-b-2 py-1.25 flex w-full items-center gap-1 border-b px-2"
+      type="button">
+      <span class="text-c-2">Discriminator</span>
+      <span class="composition-selector-label text-c-1 relative">
+        {{ discriminatorPropertyName }} {{ selectedOption?.label }}
+      </span>
+      <ScalarIconCaretDown />
+    </button>
+  </ScalarListbox>
+</template>
diff --git a/packages/api-reference/src/components/Content/Schema/SchemaPropertyHeading.vue b/packages/api-reference/src/components/Content/Schema/SchemaPropertyHeading.vue
index 36d80390c82..aff3c56b9d3 100644
--- a/packages/api-reference/src/components/Content/Schema/SchemaPropertyHeading.vue
+++ b/packages/api-reference/src/components/Content/Schema/SchemaPropertyHeading.vue
@@ -181,6 +181,11 @@ const modelName = computed(() => {
         name="name" />
       <template v-else>&sol;<slot name="name" />&sol;</template>
     </div>
+    <div
+      v-if="value?.isDiscriminator"
+      class="property-discriminator">
+      Discriminator
+    </div>
     <template v-if="value">
       <SchemaPropertyDetail v-if="value?.type">
         <ScreenReader>Type:</ScreenReader>
@@ -357,6 +362,12 @@ const modelName = computed(() => {
   font-size: var(--scalar-micro);
   color: var(--scalar-color-green);
 }
+
+.property-discriminator {
+  font-size: var(--scalar-micro);
+  color: var(--scalar-color-purple);
+}
+
 .property-detail {
   font-size: var(--scalar-micro);
   color: var(--scalar-color-2);
diff --git a/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.test.ts b/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.test.ts
index 89b86b30cf5..9fbed256dfc 100644
--- a/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.test.ts
+++ b/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.test.ts
@@ -1,7 +1,12 @@
 import { describe, expect, it } from 'vitest'
 import type { OpenAPIV3_1 } from '@scalar/openapi-types'
 
-import { getDiscriminatorPropertyName } from './schema-discriminator'
+import {
+  getDiscriminatorMapping,
+  getDiscriminatorPropertyName,
+  hasDiscriminator,
+  mergeDiscriminatorSchemas,
+} from './schema-discriminator'
 
 describe('schema-discriminator', () => {
   describe('getDiscriminatorPropertyName', () => {
@@ -19,4 +24,150 @@ describe('schema-discriminator', () => {
       expect(getDiscriminatorPropertyName(schema)).toBeUndefined()
     })
   })
+
+  describe('getDiscriminatorMapping', () => {
+    it('returns mapping from discriminator', () => {
+      const schema: OpenAPIV3_1.SchemaObject = {
+        discriminator: {
+          propertyName: 'type',
+          mapping: {
+            terrestrial: '#/components/schemas/TerrestrialPlanet',
+            gas_giant: '#/components/schemas/GasGiant',
+          },
+        },
+      }
+      expect(getDiscriminatorMapping(schema)).toEqual({
+        terrestrial: '#/components/schemas/TerrestrialPlanet',
+        gas_giant: '#/components/schemas/GasGiant',
+      })
+    })
+  })
+
+  describe('hasDiscriminator', () => {
+    it('returns true when schema has discriminator', () => {
+      const schema: OpenAPIV3_1.SchemaObject = {
+        discriminator: {
+          propertyName: 'type',
+        },
+      }
+      expect(hasDiscriminator(schema)).toBe(true)
+    })
+
+    it('returns false when schema has no discriminator', () => {
+      const schema: OpenAPIV3_1.SchemaObject = {}
+      expect(hasDiscriminator(schema)).toBe(false)
+    })
+  })
+
+  describe('mergeDiscriminatorSchemas', () => {
+    it('merges base schema with selected type schema', () => {
+      const baseSchema: OpenAPIV3_1.SchemaObject = {
+        type: 'object',
+        required: ['name', 'type'],
+        properties: {
+          name: { type: 'string' },
+          type: { type: 'string' },
+        },
+        discriminator: {
+          propertyName: 'type',
+          mapping: {
+            terrestrial: '#/components/schemas/TerrestrialPlanet',
+          },
+        },
+      }
+
+      const schemas: Record<string, OpenAPIV3_1.SchemaObject> = {
+        TerrestrialPlanet: {
+          type: 'object',
+          required: ['atmosphere'],
+          properties: {
+            atmosphere: {
+              type: 'array',
+              items: {
+                type: 'object',
+                properties: {
+                  compound: { type: 'string' },
+                  percentage: { type: 'number' },
+                },
+              },
+            },
+          },
+        },
+      }
+
+      const result = mergeDiscriminatorSchemas(baseSchema, 'terrestrial', schemas)
+      expect(result).toEqual({
+        type: 'object',
+        required: ['name', 'type', 'atmosphere'],
+        properties: {
+          name: { type: 'string' },
+          type: { type: 'string' },
+          atmosphere: {
+            type: 'array',
+            items: {
+              type: 'object',
+              properties: {
+                compound: { type: 'string' },
+                percentage: { type: 'number' },
+              },
+            },
+          },
+        },
+      })
+    })
+
+    it('merges schema with allOf composition', () => {
+      const baseSchema: OpenAPIV3_1.SchemaObject = {
+        type: 'object',
+        required: ['type'],
+        properties: {
+          type: { type: 'string' },
+        },
+        discriminator: {
+          propertyName: 'type',
+          mapping: {
+            gas_giant: '#/components/schemas/GasGiantWithAllOf',
+          },
+        },
+      }
+
+      const schemas: Record<string, OpenAPIV3_1.SchemaObject> = {
+        GasGiantWithAllOf: {
+          type: 'object',
+          allOf: [
+            {
+              type: 'object',
+              required: ['mass'],
+              properties: {
+                mass: { type: 'number', format: 'float' },
+              },
+            },
+            {
+              type: 'object',
+              required: ['satellites'],
+              properties: {
+                satellites: {
+                  type: 'array',
+                  items: { type: 'object' },
+                },
+              },
+            },
+          ],
+        },
+      }
+
+      const result = mergeDiscriminatorSchemas(baseSchema, 'gas_giant', schemas)
+      expect(result).toEqual({
+        type: 'object',
+        properties: {
+          mass: { type: 'number', format: 'float' },
+          satellites: {
+            type: 'array',
+            items: { type: 'object' },
+          },
+        },
+        required: ['type', 'mass', 'satellites'],
+      })
+    })
+  })
 })
diff --git a/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.ts b/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.ts
index 0e09c9144c1..5e47cd03861 100644
--- a/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.ts
+++ b/packages/api-reference/src/components/Content/Schema/helpers/schema-discriminator.ts
@@ -1,6 +1,89 @@
 import type { OpenAPIV3_1 } from '@scalar/openapi-types'
 
+export type DiscriminatorMapping = NonNullable<OpenAPIV3_1.DiscriminatorObject['mapping']>
+
+export type DiscriminatorPropertyName = OpenAPIV3_1.DiscriminatorObject['propertyName']
+
+/** Get discriminator mapping from schema */
+export function getDiscriminatorMapping(schema: OpenAPIV3_1.SchemaObject): DiscriminatorMapping | undefined {
+  if (!schema.discriminator?.mapping) {
+    return undefined
+  }
+
+  return schema.discriminator.mapping
+}
+
 /** Get discriminator property name from schema */
-export function getDiscriminatorPropertyName(schema: OpenAPIV3_1.SchemaObject): string | undefined {
+export function getDiscriminatorPropertyName(schema: OpenAPIV3_1.SchemaObject): DiscriminatorPropertyName | undefined {
   return schema.discriminator?.propertyName
 }
+
+/** Check if schema has a discriminator */
+export function hasDiscriminator(schema: OpenAPIV3_1.SchemaObject): boolean {
+  return schema.discriminator !== undefined
+}
+
+/** Merge discriminator schemas */
+export function mergeDiscriminatorSchemas(
+  baseSchema: OpenAPIV3_1.SchemaObject,
+  selectedType: string,
+  schemas: Record<string, OpenAPIV3_1.SchemaObject>,
+): OpenAPIV3_1.SchemaObject | undefined {
+  if (!baseSchema.discriminator?.mapping || !selectedType) {
+    return undefined
+  }
+
+  const refPath = baseSchema.discriminator.mapping[selectedType]
+  if (!refPath) {
+    return undefined
+  }
+
+  // Extract schema name from ref path
+  const schemaName = refPath.split('/').pop()
+  if (!schemaName || !schemas[schemaName]) {
+    return undefined
+  }
+
+  const selectedSchema = schemas[schemaName]
+
+  // If the selected schema has allOf, merge all subschemas
+  if (selectedSchema.allOf) {
+    const mergedSchema: OpenAPIV3_1.SchemaObject = {
+      type: selectedSchema.type,
+      properties: {},
+      required: [],
+    }
+    const properties: Record<string, OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject> = {}
+    const required = new Set<string>()
+
+    // First add base schema required fields
+    if (baseSchema.required) {
+      baseSchema.required.forEach((field: string) => required.add(field))
+    }
+
+    // Then process all allOf schemas
+    for (const subSchema of selectedSchema.allOf) {
+      if ('properties' in subSchema) {
+        Object.assign(properties, subSchema.properties)
+      }
+      if ('required' in subSchema && Array.isArray(subSchema.required)) {
+        subSchema.required.forEach((field: string) => required.add(field))
+      }
+    }
+
+    mergedSchema.properties = properties
+    mergedSchema.required = Array.from(required)
+
+    return mergedSchema
+  }
+
+  // If no allOf, merge with base schema
+  return {
+    type: selectedSchema.type || baseSchema.type,
+    properties: {
+      ...(baseSchema.properties || {}),
+      ...(selectedSchema.properties || {}),
+    },
+    required: [...(baseSchema.required || []), ...(selectedSchema.required || [])],
+  } as OpenAPIV3_1.SchemaObject
+}


Reviews:
user: login: amritk
body: SO we been adding to this quite a bit to fix each issue and its getting a bit bloated. I think we write extensive tests, and once we switch to new store we can re-write this much more efficiently and type safe. Approving for now though!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T17:48:08Z
user: login: antlio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T17:55:22Z
user: login: cameronrohani
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T19:15:04Z

Review comments:
diff_hunk: @@ -36,9 +42,58 @@ const props = withDefaults(
       | Record<string, OpenAPIV3_1.SchemaObject>
       | unknown
   }>(),
-  { level: 0, showAdditionalProperties: false, noncollapsible: false },
+  { level: 0, noncollapsible: false },
 )
 
+const selectedDiscriminatorType = ref<string>('')
+
+/** Returns the discriminator mapping */
+const discriminatorMapping = computed(() => {
+  if (!props.value) {
+    return undefined
+  }
+
+  return getDiscriminatorMapping(props.value as OpenAPIV3_1.SchemaObject)
path: packages/api-reference/src/components/Content/Schema/Schema.vue
user: login: amritk
body: The `as` should be in the `getDiscriminatorMapping` method if its a type guard
created_at: 2025-05-23T17:42:44Z
author_association: MEMBER
diff_hunk: @@ -36,9 +42,58 @@ const props = withDefaults(
       | Record<string, OpenAPIV3_1.SchemaObject>
       | unknown
   }>(),
-  { level: 0, showAdditionalProperties: false, noncollapsible: false },
+  { level: 0, noncollapsible: false },
 )
 
+const selectedDiscriminatorType = ref<string>('')
+
+/** Returns the discriminator mapping */
+const discriminatorMapping = computed(() => {
+  if (!props.value) {
+    return undefined
+  }
+
+  return getDiscriminatorMapping(props.value as OpenAPIV3_1.SchemaObject)
path: packages/api-reference/src/components/Content/Schema/Schema.vue
user: login: amritk
body: technically here we are overwriting undefined which is bad
created_at: 2025-05-23T17:43:03Z
author_association: MEMBER
diff_hunk: @@ -36,9 +42,58 @@ const props = withDefaults(
       | Record<string, OpenAPIV3_1.SchemaObject>
       | unknown
   }>(),
-  { level: 0, showAdditionalProperties: false, noncollapsible: false },
+  { level: 0, noncollapsible: false },
 )
 
+const selectedDiscriminatorType = ref<string>('')
+
+/** Returns the discriminator mapping */
+const discriminatorMapping = computed(() => {
+  if (!props.value) {
+    return undefined
+  }
+
+  return getDiscriminatorMapping(props.value as OpenAPIV3_1.SchemaObject)
path: packages/api-reference/src/components/Content/Schema/Schema.vue
user: login: antlio
body: oups its already the case, forgot to remove this
created_at: 2025-05-23T17:55:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-23T09:38:15Z
body: ###  🦋  Changeset detected

Latest commit: e920bf34a04e44ec9c272ddaee8e9784115a1c48

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 5 packages</summary>

  | Name                          | Type  |
| ----------------------------- | ----- |
| @scalar/api-reference         | Patch |
| @scalar/aspnetcore            | Patch |
| @scalar/fastify-api-reference | Patch |
| @scalar/nuxt                  | Patch |
| @scalar/api-reference-react   | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/scalar/scalar/new/anthony/feat-api-reference-discriminator-support?filename=.changeset/blue-kings-poke.md&value=---%0A%22%40scalar%2Fapi-reference%22%3A%20patch%0A---%0A%0Afeat(api-reference)%3A%20discriminator%20support%0A)


author_association: NONE
user: login: relativeci[bot]
created_at: 2025-05-23T09:41:05Z
body: ## [#10001](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B?utm_source=github&utm_campaign=pr-report&utm_content=title "Bundle Size — 2.42MiB (+0.11%).") Bundle Size — 2.42MiB (+0.11%).
e920bf3(current) vs 9ffba73 **[main#10000](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10000-8YDr5mYuRrQHKyyqjgPR?utm_source=github&utm_content=report-header&utm_campaign=pr-report "View baseline job report")**(baseline)

> [!WARNING]
> Bundle contains 3 duplicate packages – [View duplicate packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/packages?bp=%7B%22filters%22%3A%22duplicate-1%22%7D&utm_source=github&utm_campaign=pr-report&utm_content=insights)
<details>
<summary><strong>Bundle metrics</strong>&nbsp;&nbsp;<img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center"> <code>2 changes</code> <img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center"> <code>1 regression</code></summary>
<br>


|            |       Current<br />[#10001](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B?utm_source=github&utm_content=summary&utm_campaign=pr-report "View bundle analysis report") |      Baseline<br />[#10000](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10000-8YDr5mYuRrQHKyyqjgPR?utm_source=github&utm_content=summary&utm_campaign=pr-report "View baseline bundle analysis report") |
|:--|--:|--:|
| <strong><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center">&nbsp;&nbsp;[Initial JS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-1_et.chunk-0_et.asset-0_ft.CSS-0_ft.JS-1_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View initial JS assets")</strong> | <strong>`2.42MiB`(`+0.11%`)</strong> | <strong>`2.42MiB`</strong> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Initial CSS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-1_et.chunk-0_et.asset-0_ft.CSS-1_ft.JS-0_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View initial CSS assets")</span> | <span>`0B`</span> | <span>`0B`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Cache Invalidation](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/assets?ba=%7B%22filters%22%3A%22changed-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View changed assets")</span> | <span>`100%`</span> | <span>`100%`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Chunks](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-0_et.chunk-1_et.asset-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View chunks")</span> | <span>`1`</span> | <span>`1`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Assets](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/assets?ba=%7B%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View assets")</span> | <span>`1`</span> | <span>`1`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center">&nbsp;&nbsp;[Modules](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/modules?bm=%7B%22metric%22%3A%22totalSize%22%2C%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View modules")</span> | <span>`1493`(`+0.2%`)</span> | <span>`1490`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Modules](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/modules?bm=%7B%22metric%22%3A%22duplicateSize%22%2C%22filters%22%3A%22changed-0_md-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate modules")</span> | <span>`0`</span> | <span>`0`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Code](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/modules?bm=%7B%22metric%22%3A%22duplicateSize%22%2C%22filters%22%3A%22changed-0_md-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate modules")</span> | <span>`0%`</span> | <span>`0%`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/packages?bp=%7B%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View packages")</span> | <span>`168`</span> | <span>`168`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/packages?bp=%7B%22filters%22%3A%22duplicate-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate packages")</span> | <span>`3`</span> | <span>`3`</span> |
</details>


<details>
<summary><strong>Bundle size by type</strong>&nbsp;&nbsp;<img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center"> <code>1 change</code> <img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center"> <code>1 regression</code></summary>
<br>


|            |       Current<br />[#10001](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B?utm_source=github&utm_content=totals&utm_campaign=pr-report "View bundle analysis report") |      Baseline<br />[#10000](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10000-8YDr5mYuRrQHKyyqjgPR?utm_source=github&utm_content=totals&utm_campaign=pr-report "View baseline bundle analysis report") |
|:--|--:|--:|
| <strong><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center">&nbsp;&nbsp;[JS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B/assets?ba=%7B%22filters%22%3A%22ft.CSS-0_ft.JS-1_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=totals&utm_campaign=pr-report "View JS assets")</strong> | <strong>`2.42MiB` (`+0.11%`)</strong> | <strong>`2.42MiB`</strong> |
</details>


[Bundle analysis report](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/10001-VQuiJbm6yMqGrSRZKJ3B?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Branch **anthony/feat-api-reference-discr...**](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/tree/anthony%2Ffeat-api-reference-discriminator-support?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Project dashboard](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX?utm_source=github&utm_content=report-footer&utm_campaign=pr-report)---<sup>Generated by [RelativeCI](https://relative-ci.com/?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Documentation](https://relative-ci.com/documentation?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Report issue](https://github.com/relative-ci/roadmap/issues)</sup>
<!--RELATIVE_CI_PROJECT_NAME="scalar"-->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-23T09:41:39Z
body: **Preview Examples**

https://a677ea79-1508-4934-af13-5eb12d95dec1--scalar-deploy-preview.netlify.app
<!-- thollander/actions-comment-pull-request "netlify-preview" -->
author_association: CONTRIBUTOR