API Reference¶
The Pydantic Tornado API is more complex than I would like it to be, but it is also very powerful. The combination of compile-time and runtime behavior makes the API rather complex to reason about. Hopefully, this section will help you understand how to use the API. The snippets in this section refer back to the example application presented in the main page.
Decorators¶
Note
The add_error_response
and add_response_header
decorators do not insert headers or influence the response sent
to the client. They only add metadata to the OpenAPI specification. The request handler is responsible for calling
self.set_header to set the header content.
The api.expose_operation decorator exposes a method as an operation in the OpenAPI
specification. It also validates the request body and other parameters based on type annotations and then passes the validated
values to the method. The method is required to be an async method and it should return a model instance or None
. The
model schema for the body and the "successful" return value are taken directly from the Pydantic models used in the type
annotations. You can influence the schema definition using Pydantic's JSON schema generation features.
The api.add_error_response decorator adds an error response to the operation. Each
decorator call adds a new error response to the responses
collection
of the OpenAPI operation. You can include multiple error responses for a single operation by stacking decorators. This decorator
works in conjunction with the register_error_model method
of the application. You can specify the structure (schema) of the error body using the model
keyword argument of the decorator
or by calling the register_error_model
method during application initialization.
Tip
You should default to describing error structure using the register_error_model
method. The model
keyword argument of the api.add_error_response decorator should
only be used for responses that are specific to a single operation.
The api.add_response_header decorator adds a response header to the specification.
Most response headers have a simple structure, but you can use the model
parameter to specify a more complex structure if
necessary. The for_status
keyword can be used to specify the status code for which the header is relevant. If the for_status
is unspecified, then the header is added to every response for the operation. It can be set to a single status code or a list
of status codes.
class CollectionHandler(RequestHandler):
@api.expose_operation(summary='Create a new Item', default_status=201)
@api.add_error_response(422)
@api.add_response_header('location', for_status=201)
async def post(
self, *, body: typing.Annotated[CreationRequest, api.Body]
) -> Item:
new_item = Item(id=uuid.uuid4(), **body.model_dump(mode='python'))
self.application.db[new_item.id] = new_item
self.set_header('location', self.reverse_url('item_handler', new_item.id))
return new_item
Usage of typing.Annotated¶
This library makes use of typing.Annotated to attach OpenAPI metadata to types and to mark parameters for runtime processing.
I adopted this approach after using FastAPI's dependency injection system. The use of annotations frees you to name and arrange
your method parameters freely. In essence, this is what makes your request handling methods feel like regular Python methods
where you are in complete control of the signature. The magic happens inside the erapper provided by the api.expose_operation
decorator so you can call the method exactly as it appears in the source code should you need to do so.
Marking the request body¶
class ItemHandler(RequestHandler):
@api.expose_operation(summary='Update Item by ID')
@api.add_error_response(404, description='Item Not Found')
@api.add_error_response(422)
async def put(
self, item_id: str, *, body: typing.Annotated[Item, api.Body]
) -> Item:
...
The body
parameter is marked with typing.Annotated[Item, api.Body]
. This tells the API that the parameter should receive the
request body. The library will validate the request body against the Item
model and pass the validated data to the method.
The pydantic model is also used to generate the OpenAPI schema for the request body. The item_id
parameter is not marked with
a pydantic-tornado annotation so it is treated as a path parameter. The type hint is used as the schema for the path parameter
in the generated OpenAPI operation. Path parameters are passed as strings; however, the library converts the string value to the
hinted type.
Warning
The library only supports str
, int
, and float
path parameters. If you need to use a different type, you should use
a string annotation and implement type conversion in the method body.
Augmenting the OpenAPI specification¶
The second use of typing.Annotated
is to augment the generated OpenAPI specification. You can pass an instance of
api.Body as an annotation to specify additional metadata for the request body. The description for
the request body is omitted by default. You can provide one by passing the description
keyword to the api.Body
initializer.
class ItemHandler(RequestHandler):
@api.expose_operation(summary='Update Item by ID')
@api.add_error_response(404, description='Item Not Found')
@api.add_error_response(422)
async def put(
self, item_id: str, *,
body: typing.Annotated[Item, api.Body(description='The new item data')]
) -> Item:
...
Request bodies and response types¶
The request body and response types are specified using Pydantic models. The models are used to validate the request body and generate the OpenAPI schema for the operation. If the request model does not validate the request body, the API will return a 422 Unprocessable Entity response code with a JSON body containing the validation errors. The response body uses the openapi.ValidationError model. The library does not register the error model for you. You should add the api.add_error_response decorator to your method if you want to document the response code.
OpenAPI specification and UI¶
The OpenAPI specification is generated from information stored on the Application
instance which must include
handlers.OpenAPIApplication in its MRO. The specification is made available by
installing a route using the handlers.OpenAPISpecHandler handler. The
handlers.OpenAPIDocHandler handler is also available to serve a
Stoplight Elements documentation page. You are responsible for wiring the handlers together
in your application. I recommend subclassing the Application
class and extending the initializer instead of using a factory
function.
from pydantictornado import handlers
from tornado import web
class Application(handlers.OpenAPIApplication, web.Application):
def __init__(self, *args, **kwargs):
routes = [
web.URLSpec(r'/openapi.json', handlers.OpenAPISpecHandler, name='openapi_spec'),
web.URLSpec(r'/docs', handlers.OpenAPIDocHandler,
kwargs={'spec_handler_name': 'openapi_spec'}, name='openapi_docs'),
]
super().__init__(routes, *args, **kwargs)