connexion
connexion copied to clipboard
Unable to use oneOf in response for schemas which are polymorphic
Description
When validate_responses
is set to True, I can't use oneOf when describing the response's content for schemas which are polymorphic.
I have 2 models defined with SQLAlchemy, User and AdminUser (see code below).
AdminUser extends User and has an additional field called privilege. The same models are defined in OpenAPI side.
If I set validate_response
to False, I can get the expected result (see below). When validate_response
is set True, I get an 500 error when access the endpoint instead.
Expected behaviour
When access /user/{id}
For a User
{
"id": 1,
"user_type": "normal",
"username": "theos"
}
For an AdminUser
{
"id": 2,
"privilege": "shutdown the system",
"user_type": "admin",
"username": "jack"
}
Actual behaviour
{
"detail": "{'id': 1, 'user_type': 'normal', 'username': 'theos'} is valid under each of {'x-scope': ['', '#/components/responses/UserResponse'], 'allOf': [{'x-scope': ['', '#/components/responses/TimeEntriesResponse', '#/components/schemas/TimeEntry', '#/components/schemas/Admin'], 'type': 'object', 'required': ['id', 'username', 'user_type'], 'properties': {'id': {'type': 'integer'}, 'username': {'type': 'string'}, 'user_type': {'type': 'string'}}, 'discriminator': {'propertyName': 'user_type', 'mapping': {'normal': '#/components/schemas/User', 'admin': '#/components/schemas/Admin'}}}, {'type': 'object', 'properties': {'privilege': {'type': 'string'}}}]}, {'x-scope': ['', '#/components/responses/UserResponse'], 'type': 'object', 'required': ['id', 'username', 'user_type'], 'properties': {'id': {'type': 'integer'}, 'username': {'type': 'string'}, 'user_type': {'type': 'string'}}, 'discriminator': {'propertyName': 'user_type', 'mapping': {'normal': '#/components/schemas/User', 'admin': '#/components/schemas/Admin'}}}\n\nFailed validating 'oneOf' in schema:\n {'components': {'parameters': {'FieldsToUpdate': {'description': 'The '\n 'fields '\n 'to '\n 'update',\n 'in': 'query',\n 'name': 'fields',\n 'required': True,\n 'schema': {'items': {'type': 'string'},\n 'type': 'array'}},\n 'Id': {'in': 'path',\n 'name': 'id',\n 'required': True,\n 'schema': {'type': 'integer'}},\n 'Limit': {'description': 'The number of '\n 'records to '\n 'return',\n 'in': 'query',\n 'name': 'limit',\n 'schema': {'default': 30,\n 'maximum': 50,\n 'minimum': 1,\n 'type': 'integer'}},\n 'Offset': {'description': 'The number '\n 'of records '\n 'to skip',\n 'in': 'query',\n 'name': 'offset',\n 'schema': {'default': 0,\n 'minimum': 0,\n 'type': 'integer'}}},\n 'requestBodies': {'TimeEntryRequestBody': {'content': {'application/json': {'schema': {'components': <Recursion on dict with id=2299089266624>,\n 'properties': {'category': {'nullable': True,\n 'type': 'string'},\n 'ended_at': {'format': 'date-time',\n 'type': 'string'},\n 'id': {'type': 'integer'},\n 'started_at': {'format': 'date-time',\n 'type': 'string'},\n 'tags': {'items': {'properties': {'content': {'type': 'string'},\n 'id': {'type': 'integer'}},\n 'required': ['id',\n 'content'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n 'type': 'array'},\n 'title': {'type': 'string'},\n 'user': {'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']}]},\n 'user_id': {'type': 'integer'}},\n 'required': ['id',\n 'title',\n 'started_at',\n 'ended_at'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/requestBodies/TimeEntryRequestBody']}}},\n 'description': 'TimeEntry '\n 'request '\n 'body'}},\n 'responses': {'TimeEntriesResponse': {'content': {'application/json': {'schema': {'items': {'properties': {'category': {'nullable': True,\n 'type': 'string'},\n 'ended_at': {'format': 'date-time',\n 'type': 'string'},\n 'id': {'type': 'integer'},\n 'started_at': {'format': 'date-time',\n 'type': 'string'},\n 'tags': {'items': {'properties': {'content': {'type': 'string'},\n 'id': {'type': 'integer'}},\n 'required': ['id',\n 'content'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n 'type': 'array'},\n 'title': {'type': 'string'},\n 'user': {'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']}]},\n 'user_id': {'type': 'integer'}},\n 'required': ['id',\n 'title',\n 'started_at',\n 'ended_at'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse']},\n 'type': 'array'}}},\n 'description': 'A '\n 'list '\n 'of '\n 'TimeEntry'},\n 'TimeEntryResponse': {'content': {'application/json': {'schema': {'properties': {'category': {'nullable': True,\n 'type': 'string'},\n 'ended_at': {'format': 'date-time',\n 'type': 'string'},\n 'id': {'type': 'integer'},\n 'started_at': {'format': 'date-time',\n 'type': 'string'},\n 'tags': {'items': {'properties': {'content': {'type': 'string'},\n 'id': {'type': 'integer'}},\n 'required': ['id',\n 'content'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n 'type': 'array'},\n 'title': {'type': 'string'},\n 'user': {'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']}]},\n 'user_id': {'type': 'integer'}},\n 'required': ['id',\n 'title',\n 'started_at',\n 'ended_at'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntryResponse']}}},\n 'description': 'The '\n 'JSON '\n 'representation '\n 'of '\n 'a '\n 'TimeEntry'},\n 'UserResponse': {'content': {'application/json': {'schema': <Recursion on dict with id=2299089298624>}},\n 'description': 'A User'},\n 'UsersResponse': {'content': {'application/json': {'schema': {'items': {'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/UsersResponse']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['',\n '#/components/responses/UsersResponse']}]},\n 'type': 'array'}}},\n 'description': 'A list '\n 'of '\n 'User'}},\n 'schemas': {'Admin': {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}]},\n 'Tag': {'properties': {'content': {'type': 'string'},\n 'id': {'type': 'integer'}},\n 'required': ['id', 'content'],\n 'type': 'object'},\n 'TimeEntry': {'properties': {'category': {'nullable': True,\n 'type': 'string'},\n 'ended_at': {'format': 'date-time',\n 'type': 'string'},\n 'id': {'type': 'integer'},\n 'started_at': {'format': 'date-time',\n 'type': 'string'},\n 'tags': {'items': {'properties': {'content': {'type': 'string'},\n 'id': {'type': 'integer'}},\n 'required': ['id',\n 'content'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n 'type': 'array'},\n 'title': {'type': 'string'},\n 'user': {'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']}]},\n 'user_id': {'type': 'integer'}},\n 'required': ['id',\n 'title',\n 'started_at',\n 'ended_at'],\n 'type': 'object'},\n 'User': {'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object'}}},\n 'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id', 'username', 'user_type'],\n 'type': 'object',\n 'x-scope': ['', '#/components/responses/UserResponse']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id', 'username', 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['', '#/components/responses/UserResponse']},\n {'properties': {'category': {'nullable': True,\n 'type': 'string'},\n 'ended_at': {'format': 'date-time',\n 'type': 'string'},\n 'id': {'type': 'integer'},\n 'started_at': {'format': 'date-time',\n 'type': 'string'},\n 'tags': {'items': {'properties': {'content': {'type': 'string'},\n 'id': {'type': 'integer'}},\n 'required': ['id',\n 'content'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n 'type': 'array'},\n 'title': {'type': 'string'},\n 'user': {'oneOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']},\n {'allOf': [{'discriminator': {'mapping': {'admin': '#/components/schemas/Admin',\n 'normal': '#/components/schemas/User'},\n 'propertyName': 'user_type'},\n 'properties': {'id': {'type': 'integer'},\n 'user_type': {'type': 'string'},\n 'username': {'type': 'string'}},\n 'required': ['id',\n 'username',\n 'user_type'],\n 'type': 'object',\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry',\n '#/components/schemas/Admin']},\n {'properties': {'privilege': {'type': 'string'}},\n 'type': 'object'}],\n 'x-scope': ['',\n '#/components/responses/TimeEntriesResponse',\n '#/components/schemas/TimeEntry']}]},\n 'user_id': {'type': 'integer'}},\n 'required': ['id', 'title', 'started_at', 'ended_at'],\n 'type': 'object',\n 'x-scope': ['', '#/components/responses/UserResponse']}]}\n\nOn instance:\n {'id': 1, 'user_type': 'normal', 'username': 'theos'}",
"status": 500,
"title": "Response body does not conform to specification",
"type": "about:blank"
}
Steps to reproduce
My SQLAlchemy model for User and AdminUser
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(128), unique=True, nullable=False)
user_type = db.Column(db.String(128), nullable=False)
time_entries = db.relationship(TimeEntry, back_populates='user')
__mapper_args__ = {
"polymorphic_identity": UserTypeIdentifier.NORMAL.value,
"polymorphic_on": user_type,
}
class AdminUser(User):
__tablename__ = 'admin_user'
id = db.Column(db.ForeignKey('user.id'), primary_key=True)
privilege = db.Column(db.String(128))
__mapper_args__ = {
"polymorphic_identity": UserTypeIdentifier.ADMIN.value
}
Endpoint for getting a user
/user/{id}:
get:
tags:
- User
operationId: api.UserApi.get_by_id
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
$ref: '#/components/responses/UserResponse'
My UserResponse definition
UserResponse:
description: A User
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/Admin'
Schemas for User and Admin
components:
schemas:
User:
type: object
required:
- id
- username
- user_type
properties:
id:
type: integer
username:
type: string
user_type:
type: string
discriminator:
propertyName: user_type
mapping:
normal: '#/components/schemas/User'
admin: '#/components/schemas/Admin'
Admin:
# Model inheritance
allOf:
- $ref: '#/components/schemas/User'
- type: object
properties:
privilege:
type: string
Additional info:
Output of the commands:
-
python --version
-
pip show connexion | grep "^Version\:"
If I define the UserResponse as below, I can get the correct response (either a User or an AdminUser). But the problem is that in this case, the UI doesn't tell the user Admin is also a valid response.
UserResponse:
description: A User
content:
application/json:
schema:
- $ref: '#/components/schemas/User'
I am also affected by this issue.
Closing in favor of #1569.