Redocly / redoc

📘 OpenAPI/Swagger-generated API Reference Documentation
https://redocly.github.io/redoc/
MIT License
23.58k stars 2.3k forks source link

Overrides user-provided discriminator values with object names #1794

Closed rmelasavvi closed 2 years ago

rmelasavvi commented 2 years ago

Describe the bug

Happens if I define a "Vehicle", with a discriminator property of "vehicle_type", and then derive a "Car", with "vehicle_type: automobile". The discriminator dropdowns and the sample requests ignore the values I've provided and use "Car" instead of the "automobile" value I've specified.

Expected behavior

If I define a Vehicle object with a discriminator propertyName of 'vehicle_type', and then in subclass Car I provide a value of 'automobile', then I expect the discriminator dropdowns, and the vehicle_type in the sample, to display "automobile", not "Car".

Minimal reproducible OpenAPI snippet(if possible)

openapi: "3.0.1"
info:
  version: 1.0.0
  title: Vehicles
  description: Polymorphism example
servers:
  - url: https://api.example.com/vehicles/v1
paths:
  /vehicles:
    get:
      operationId: list_vehicles
      summary: List all vehicles
      responses:
        '200':
          description: An paged array of vehicles
          content:
            application/json:    
              schema:
                $ref: "#/components/schemas/Vehicle"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /vehicle:
    post:
      operationId: update_vehicle
      summary: create new vehicle
      tags:
        - vehicle 
      description: Create new vehicle
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Vehicle'

      responses:
        '200':
          description: Successfully created vehicle
          content:
            application/json:
              schema:
                type: object
                properties:
                  vehicle_id:
                    type: integer

  /vehicles/{id}:
    get:
      operationId: get_vehicle
      summary: Info for a specific vehicle
      tags:
        - vehicles
      parameters:
        - name: id
          in: path
          required: true
          description: The id of vehicle to retrieve
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Car"
                  - $ref: "#/components/schemas/Plane"
                discriminator:
                  propertyName: type
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Vehicle:
      type: object
      required:
        - id
        - vehicle_type
      properties:
        id:
          type: integer
        model:
          type: string
        name:
          type: string
        vehicle_type:
          type: string
      discriminator:
        propertyName: vehicle_type

    Car:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          properties:
            vehicle_type:
              enum:
                - automobile
            has_4_wheel_drive:
              type: boolean

    Plane:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          properties:
            vehicle_type:
              enum:
                - airplane
            has_reactor:
              type: boolean
            nb_passengers:
              type: integer

    Error:
      type: object
      properties:
        code:
          enum:
            - 400
        message:
          enum:
            - error

Screenshots Problem - I expect to see "automobile" and "airplane", not "Car" and "Plane" in the dropdowns and in the example.

redoc-discriminators-screenshot

Expected ( I've only fixed the sample component -- not the dropdowns yet ):

sorta-fixed
rmelasavvi commented 2 years ago

FWIW, here's how I resolved it for our internal use. Solution is not the cleanest, but does point to locations in the code where you might implement a fix

diff --git a/src/components/Schema/DiscriminatorDropdown.tsx b/src/components/Schema/DiscriminatorDropdown.tsx
index d3290a2a..0d155861 100644
--- a/src/components/Schema/DiscriminatorDropdown.tsx
+++ b/src/components/Schema/DiscriminatorDropdown.tsx
@@ -31,9 +31,11 @@ export class DiscriminatorDropdown extends React.Component<{
       return null;
     }

+    const discriminatorProp = parent.discriminatorProp
+
     const options = parent.oneOf.map((subSchema, idx) => {
       return {
-        value: subSchema.title,
+        value: subSchema.discriminant(discriminatorProp),
         idx,
       };
     });
diff --git a/src/services/models/MediaType.ts b/src/services/models/MediaType.ts
index 25dae807..2a1e6e20 100644
--- a/src/services/models/MediaType.ts
+++ b/src/services/models/MediaType.ts
@@ -63,10 +63,9 @@ export class MediaTypeModel {
         const sample = Sampler.sample(subSchema.rawSchema as any, samplerOptions, parser.spec);

         if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
-          sample[this.schema.discriminatorProp] = subSchema.title;
+          sample[this.schema.discriminatorProp] = subSchema.discriminant(this.schema.discriminatorProp)
         }
-
-        this.examples[subSchema.title] = new ExampleModel(
+        this.examples[subSchema.discriminant()] = new ExampleModel(
           parser,
           {
             value: sample,
diff --git a/src/services/models/Schema.ts b/src/services/models/Schema.ts
index b155d254..32294847 100644
--- a/src/services/models/Schema.ts
+++ b/src/services/models/Schema.ts
@@ -102,6 +102,26 @@ export class SchemaModel {
     this.activeOneOf = idx;
   }

+  discriminant(propName?): string {
+
+    if (!propName) {
+      propName = this.schema.discriminator?.propertyName
+
+    }
+    if (!propName) {
+      return this.title
+    }
+    const properties = this.schema.properties
+
+    if (!properties) {
+      return this.title
+    }
+
+    const prop = (properties[propName] || null)
+
+    return prop && prop.enum && prop.enum.length && prop.enum[0] || this.title
+  }
+
   hasType(type: string) {
     return this.type === type || (Array.isArray(this.type) && this.type.includes(type));
   }
RomanHotsiy commented 2 years ago

I checked your PR.

You should define mapping in this case, extracting value from enum is error-prone, it may be on different levels, e.g. it may have allOf. So it's not universal solution.

   Car:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          properties:
            vehicle_type:
              allOf:
                - enum:
                  - automobile
            has_4_wheel_drive:
              type: boolean

Use mapping instead, e.g.

components:
  schemas:
    Vehicle:
      type: object
      required:
        - id
        - vehicle_type
      properties:
        id:
          type: integer
        model:
          type: string
        name:
          type: string
        vehicle_type:
          type: string
      discriminator:
        propertyName: vehicle_type
        mapping:
          automobile: '#/components/schemas/Automobile'
          airplane:  '#/components/schemas/Airplane'