Discriminator support to be aligned with OAS3
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.
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.
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:
Any update on this?
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 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
attributesinto various nodes, such ashardwareAttributes,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.
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...