spec icon indicating copy to clipboard operation
spec copied to clipboard

Discriminator support to be aligned with OAS3

Open francocm opened this issue 1 year ago • 6 comments

Context

The overlap in specification between AsyncAPI and OpenAPI specs allows the combining of common models across both.

Example:

./models/foo.yaml
openapi: 3.1.0
info:
  title: foo-models
  description: Models for Foo
  contact:
    name: Foo Team
  version: 1.0.0
paths: { }
components:
  schemas:
    Foo:
      type: object
      properties:
        id:
          type: string
./apis/foo.openapi.ayml
openapi: "3.1.0"
info:
  title: "foo"
  description: "API Specifications for `foo`."
  version: "1.0.0"
servers:
  - url: http://localhost:8080/
    description: Local Dev
paths:
  /foo:
    get:
      summary: "Get Foos"
      description: "Get Foos"
      operationId: "getFoos"
      responses:
        "200":
          description: "OK"
          content:
            'application/json':
              schema:
                type: array
                items:
                  $ref: "../models/foo.yaml#/components/schemas/Foo"
./events/foo.asyncapi.ayml
asyncapi: 3.0.0
info:
  title: foo Events
  version: 1.0.0
servers:
  foo-local:
    host: localhost:1234
    protocol: foo
    description: Foo broker (local dev)
    security:
      - $ref: '#/components/securitySchemes/plainSecurity'
defaultContentType: application/json
channels:
  FooEvents:
    address: foo.events
    description: The topic on which foo events are published on.
    messages:
      fooQueriedEvent:
        $ref: '#/components/messages/fooQueriedEvent'
operations:
  'foo.queried':
    action: send
    channel:
      $ref: '#/channels/FooEvents'
    messages:
      - $ref: '#/channels/FooEvents/messages/fooQueriedEvent'
components:
  messages:
    fooQueriedEvent:
      name: fooQueriedEvent
      title: Foo Queried Event
      summary: Foo Queried Event
      description: Foo Queried Event
      contentType: application/json
      payload:
        $ref: '../models/foo.yaml#/components/schemas/Foo'
  securitySchemes:
    plainSecurity:
      type: plain
      description: Plain unauthenticated connection

Problem

It's quite evident that AsyncAPI in general has no compatibility issues with OpenAPI specifications.

However, the problem lies with the use of discriminator in a oneOf situation.

If I update ./models/foo.yaml to this:

openapi: 3.1.0
info:
  title: foo-models
  description: Models for Foo
  contact:
    name: Foo Team
  version: 1.0.0
paths: { }
components:
  schemas:
    Foo:
      type: object
      properties:
        id:
          type: string
        bazAttributes:
          $ref: '#/components/schemas/OneOfBazAttributes'
    OneOfBazAttributes:
      type: object
      properties:
        type:
          type: string
          enum: [ 'RealBaz', 'MockBaz' ]
      discriminator:
        propertyName: type
        mapping:
          RealBaz: '#/components/schemas/RealBazAttributes'
          MockBaz: '#/components/schemas/MockBazAttributes'
    RealBazAttributes:
      allOf:
        - $ref: '#/components/schemas/OneOfBazAttributes'
      type: object
      properties:
        bazId:
          type: string
    MockBazAttributes:
      allOf:
        - $ref: '#/components/schemas/OneOfBazAttributes'
      type: object
      properties:
        xyzId:
          type: string
Output Result
OpenAPI Works as expected
AsyncAPI Fails with "discriminator" property type must be string channels.FooEvents.messages.fooQueriedEvent.payload.properties.bazAttributes.discriminator

This is using the official asyncapi CLI binary provided by asyncapi/cli:2.6.0 docker image, but also any other AsyncAPI tool I could find.

Root Cause

From what I can understand, AsyncAPI implements discriminator using the schema that OpenAPI 2.x (Swagger) had, i.e.:

OneOfBazAttributes:
  type: object
  properties:
    type: 
      type: string
      enum: [ 'RealBazAttributes', 'MockBazAttributes' ]
  discriminator: type
# with the expectation of having the value of `type` matching
# the object name exactly as defined in the YAML

(although the AsyncAPI render does not render any oneOf options, but all validations now pass successfully).

OpenAPI moved to a newer schema in 3.x, and whilst AsyncAPI does not seem to have problems with the OpenAPI 3.x schema compatibility, the specific discriminator capability seems to still be stuck in the 2.x world.

As it is, I do not see any clean way to be able to support discriminator and both OpenAPI and AsyncAPI simultaneously, without fixing this issue, or rolling everything back to 2.x.

Proposal

Align the AsyncAPI spec around discriminator to that of OpenAPI 3.1.x, and release it as AsyncAPI 3.1.0.


Keen to see your thoughts.

Thanks anyone participating.

francocm avatar Oct 16 '24 20:10 francocm

Welcome to AsyncAPI. Thanks a lot for reporting your first issue. Please check out our contributors guide and the instructions about a basic recommended setup useful for opening a pull request.
Keep in mind there are also other channels you can use to interact with AsyncAPI community. For more details check out this issue.

github-actions[bot] avatar Oct 16 '24 20:10 github-actions[bot]

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart:

github-actions[bot] avatar Feb 14 '25 00:02 github-actions[bot]

Any update on this?

ThomasVerhoeven1998 avatar Apr 09 '25 07:04 ThomasVerhoeven1998

I'm not a user of discriminator so hard to have any opinion other than having it as just a string is already complex for me 😃

I wonder why it was never added as native keyword in JSON Schema if it is so useful

derberg avatar Jun 18 '25 10:06 derberg

@derberg thanks for your response. I can share some high-level dummy use-cases of where the use of discriminator becomes useful. I admit, I avoid it where I can, as it does make interfaces more complex than they would ideally otherwise be.

In short: thanks to discriminators you are able to easily describe sub-schemas that apply conditionally.

Example 1 - Payment Gateway Events

Event Type: payment.authorised

{
  "id": "123456",
  "paymentMethod": {
    "type": "CARD",
    "last4Digits": "1234",
    "billingCountry": "GBR"
  }
}

vs.

{
  "id": "123456",
  "paymentMethod": {
    "type": "VOUCHER",
    "voucherId": "91d449ab-da33-4c35-8c29-3ca94ab90f61"
  }
}

Example 2 - eCommerce Products

Example: product.created

{
  "sku": "FOO1234",
  "priceInfo": {
    "rrp": {
      "amountExcludingTax": 417,
      "amountIncludingTax": 500,
      "taxInfo": {
        "rate": 0.2,
        "amount": 83
      }
    }
  },
  "attributes": {
    "type": "HARDWARE",
    "media": [
      {
        "contentType": "image/png",
        "url": "https://foo"
      }
    ],
    "weight": 785
  }
}

vs.

{
  "sku": "BAZ1234",
  "priceInfo": {
    "rrp": {
      "amountExcludingTax": 417,
      "amountIncludingTax": 500,
      "taxInfo": {
        "rate": 0.2,
        "amount": 83
      }
    }
  },
  "attributes": {
    "type": "SOFTWARE",
    "icon": {
      "contentType": "image/png",
      "url": "https://foo"
    },
    "licenseType": "SUBSCRIPTION",
    "downloadable": true
  }
}

Without the use of discriminator some alternatives would be:

  • An object with all possible fields as optional (instead of required if a particular sub-schema applies) and a very good description on when to use what and hope anyone integrating uses and understands this well
  • Splitting attributes into various nodes, such as hardwareAttributes, softwareAttributes, etc, and then populate the one accordingly
  • A relaxed key-value-map allowing attributes to be free-form and anyone populates it whichever way they want, and hope for the best

It's all about trade-offs between clean JSON structure, verbosity levels, shifting schema enforcement to/from descriptions into actual spec, etc. Being able to define schema formally allows for better and simpler tooling and integration.

francocm avatar Jun 21 '25 00:06 francocm

I also use discriminators for use cases very aligned with @francocm examples (different product types have different attributes) and this discrepancy between OpenAPI and AsyncAPI is preventing an easy sharing of models between sync and async APIs.

I don't think it is relevant to question the usage of discriminator since both standards allow and support it. The question is to know whether we agree to update AsyncAPI specifications, possibly in a non-backward compatible way, for the purpose of aligning the specification of data models to what OpenAPI does. There might be a way to accept both syntax options to avoid the breaking change barrier maybe, but I am not so sure which is best...

VincentMagry avatar Oct 02 '25 12:10 VincentMagry