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 several customizations.
You can disable some specific semantic responses generation by status code:
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,))}
Or disable semantic responses completely:
Pass semantic_responses parameter
to modify() or validate().
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'
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"
},
"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_responses attribute.
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'
10
OpenAPI 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
via semantic_responses setting.
1>>> from dmr.settings import Settings, DMR_SETTINGS
2
3>>> DMR_SETTINGS = {Settings.semantic_responses: False}
Excluding views from OpenAPI#
You can exclude individual endpoints, controllers, or entire routers from OpenAPI. Excluded routes continue to work normally (including schema validation), but are not visible in the generated specification.
We support three levels of configuration with this feature:
Pass ignore_from_spec parameter
to modify() or validate()
on endpoints that you want to ignore.
1from dmr import Controller, modify
2from dmr.plugins.pydantic import PydanticSerializer
3
4
5class BillController(Controller[PydanticSerializer]):
6 ignore_from_spec = True
7
8 @modify(ignore_from_spec=True)
9 async def get(self) -> str:
10 return 'It works!'
11
Run result
$ curl http://127.0.0.1:8000/api/username/ -X GET
"It works!"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {}
}
Customize ignore_from_spec attribute.
Endpoints can override this value with ignore_from_spec parameter.
1from dmr import Controller, modify
2from dmr.plugins.pydantic import PydanticSerializer
3
4
5class BillController(Controller[PydanticSerializer]):
6 ignore_from_spec = True
7
8 def get(self) -> str:
9 return 'It is hidden'
10
11 @modify(ignore_from_spec=False)
12 def post(self) -> str:
13 return 'It is in OpenAPI'
14
Run result
$ curl http://127.0.0.1:8000/api/username/ -X GET
"It is hidden"
$ curl http://127.0.0.1:8000/api/username/ -X POST
"It is in OpenAPI"
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/billcontroller/": {
"post": {
"deprecated": false,
"operationId": "postBillcontrollerApiBillcontroller",
"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"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Pass ignore_from_spec parameter
to Router to exclude all routes from this router.
When routers are nested, ignore_from_spec excludes the whole
router subtree. Runtime URL routing is not affected.
1from dmr.openapi import build_schema
2from dmr.openapi.views import OpenAPIJsonView
3from dmr.routing import Router, path
4from examples.getting_started.msgspec_controller import UserController
5
6private_router = Router(
7 'private/',
8 [
9 path('users/', UserController.as_view()),
10 ],
11 ignore_from_spec=True,
12)
13
14router = Router(
15 'api/',
16 [
17 path('users/', UserController.as_view()),
18 ],
19)
20router.include(private_router)
21
22schema = build_schema(router)
23
24urlpatterns = [
25 router.to_urlpatterns(namespace='api'),
26 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema)),
27]
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"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"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"UserCreateModel": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "UserCreateModel",
"type": "object"
},
"UserModel": {
"properties": {
"email": {
"type": "string"
},
"uid": {
"format": "uuid",
"type": "string"
}
},
"required": [
"email",
"uid"
],
"title": "UserModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/users/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsers",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreateModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"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"
}
}
}
}
}
}