Semantic schema¶
django-modern-rest has a lot of special features around generating
internal schema for the response validation.
The same schema is later used to build OpenAPI spec.
Our design goal is to validate the most semantic schema possible.
Semantic schema generation¶
First of all, what is a semantic schema? We define it as a schema that knows all the semantics of the given API.
What response schemas can it return?
What content types?
What status codes?
Which cookies and headers can it set?
In many frameworks these details are not important. However, in our experience – these details are very important when dealing with any big project / integration.
How do we build this semantic schema?
We enforce request and response validation. No status codes that are not specified in the schema are allowed. No extra / missing headers, no extra / missing cookies. If something goes against the schema – it is rejected by the validation
We try to make the schema building process user-friendly. For example, when you add auth to your endpoint, auth instance will inject its part of the schema into the main one. This way you will see
401response in the schema for all the endpoints which use auth. We surely allow to redefine any of this behavior
Note
We allow users to make their schemas as dumb as regular ones
with just a single setting: semantic_responses.
Turn it off together with dmr.settings.Settings.validate_responses
if you don’t need any of this schema stuff.
You would still have the very basic OpenAPI schema,
it would be similar to ones that FastAPI and others provide.
If you want to disable only some status code, use
exclude_semantic_responses.
The core part of the schema generation
is dmr.metadata.EndpointMetadata.collect_response_specs()
which collects all the responses’ metadata in a single place.
Each ResponseSpec knows what it returns in great detail.
Customizing schema generation¶
All endpoints by default generate semantic responses. However, we allow 4 levels of customizations.
First non None value wins:
Pass
semantic_responsesparameter tomodify()orvalidate().views.py¶[source]1from dmr import Controller, modify 2from dmr.plugins.pydantic import PydanticSerializer 3 4 5class APIController(Controller[PydanticSerializer]): 6 def get(self) -> str: 7 return 'will have semantic responses' 8 9 @modify(semantic_responses=False) 10 def post(self) -> str: 11 return 'will not have semantic responses' 12OpenAPI Schema
Preview openapi.json
{ "components": { "schemas": { "ErrorDetail": { "description": "Base schema for error details description.", "properties": { "loc": { "items": { "anyOf": [ { "type": "integer" }, { "type": "string" } ] }, "title": "Loc", "type": "array" }, "msg": { "title": "Msg", "type": "string" }, "type": { "title": "Type", "type": "string" } }, "required": [ "msg" ], "title": "ErrorDetail", "type": "object" }, "ErrorModel": { "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.", "properties": { "detail": { "items": { "$ref": "#/components/schemas/ErrorDetail" }, "title": "Detail", "type": "array" } }, "required": [ "detail" ], "title": "ErrorModel", "type": "object" } }, "securitySchemes": {} }, "info": { "title": "Django Modern Rest", "version": "0.1.0" }, "openapi": "3.2.0", "paths": { "/api/apicontroller/": { "get": { "deprecated": false, "operationId": "getApicontrollerApiApicontroller", "responses": { "200": { "content": { "application/json": { "schema": { "type": "string" } } }, "description": "OK" }, "406": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" } } }, "description": "Raised when provided `Accept` header cannot be satisfied" }, "422": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" } } }, "description": "Raised when returned response does not match the response schema" } } }, "post": { "deprecated": false, "operationId": "postApicontrollerApiApicontroller", "responses": { "201": { "content": { "application/json": { "schema": { "type": "string" } } }, "description": "Created" } } } } } }Customize
semantic_responsesattribute.views.py¶[source]1from dmr import Controller 2from dmr.plugins.pydantic import PydanticSerializer 3 4 5class APIController(Controller[PydanticSerializer]): 6 semantic_responses = False 7 8 async def get(self) -> str: 9 return 'will not have semantic responses' 10OpenAPI Schema
Preview openapi.json
{ "components": { "schemas": {}, "securitySchemes": {} }, "info": { "title": "Django Modern Rest", "version": "0.1.0" }, "openapi": "3.2.0", "paths": { "/api/apicontroller/": { "get": { "deprecated": false, "operationId": "getApicontrollerApiApicontroller", "responses": { "200": { "content": { "application/json": { "schema": { "type": "string" } } }, "description": "OK" } } } } } }Disable semantic responses globally:
settings.py¶1>>> from dmr.settings import Settings, DMR_SETTINGS 2 3>>> DMR_SETTINGS = {Settings.semantic_responses: False}
Pass exclude_semantic_responses parameter
to modify() or validate().
1from http import HTTPStatus
2
3from dmr import Controller, modify
4from dmr.plugins.pydantic import PydanticSerializer
5
6
7class APIController(Controller[PydanticSerializer]):
8 def get(self) -> str:
9 return 'will have semantic responses'
10
11 @modify(exclude_semantic_responses={HTTPStatus.UNPROCESSABLE_ENTITY})
12 def post(self) -> str:
13 return 'will not have semantic response with 422 status code'
14
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"deprecated": false,
"operationId": "getApicontrollerApiApicontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"post": {
"deprecated": false,
"operationId": "postApicontrollerApiApicontroller",
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Created"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
}
}
}
}
}
}
Customize exclude_semantic_responses attribute.
1from http import HTTPStatus
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticSerializer
5
6
7class APIController(Controller[PydanticSerializer]):
8 exclude_semantic_responses = frozenset((HTTPStatus.UNPROCESSABLE_ENTITY,))
9
10 async def get(self) -> str:
11 return 'will not have semantic response with 422 status code'
12
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"deprecated": false,
"operationId": "getApicontrollerApiApicontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
}
}
}
}
}
}
Exclude some semantic responses globally:
1>>> from dmr.settings import Settings, DMR_SETTINGS
2
3>>> DMR_SETTINGS = {Settings.exclude_semantic_responses: frozenset((422,))}