Usage
Contents
Usage¶
Supported Frameworks¶
The supported frameworks currently include:
Flask
There are plans to support the following frameworks:
Django
Tornado
Bottle
Other frameworks supported by webargs
Active Framework¶
specargs checks the Python environment for the frameworks mentioned in Supported Frameworks. Usage of specargs is dependent on which of these frameworks is installed in the current environment. If more than one of these frameworks is detected, an error will be raised, as selection of a specific framework when multiple are present is currently not supported. If only one is detected, that framework is set as the active framework.
Initializing a Specification¶
Generating a specification is accomplished similarly to apispec. A WebargsAPISpec must
be instantiated and provided an instance of WebargsPlugin using the plugins
keyword argument:
from specargs import WebargsAPISpec, WebargsPlugin
spec = WebargsAPISpec(
title="Example API Spec",
version="1.0.0",
openapi_version="3.0.2",
plugins=[WebargsPlugin()],
servers=[
{"url": "http://localhost:5000"}, # If testing locally
{"url": "http://dev-server-url"}
]
)
Adding Paths and Operations¶
Adding paths with operations can be accomplished using the create_paths() method
of the WebargsAPISpec class. This method accepts one argument, framework_obj. The type of
object accepted for this argument is dependent on the current active framework (as mentioned in Active Framework). The frameworks and accepted objects are as follows:
Flask:
flask.Flask
For example, paths and operations can be generated from a Flask application like so:
from flask import Flask
from specargs import WebargsAPISpec, WebargsPlugin
app = Flask(__name__, static_folder=None)
spec = WebargsAPISpec(..., plugins=[WebargsPlugin()])
...
# Register views to app
...
spec.create_paths(app)
Adding Path Parameter Metadata¶
When a framework_obj is passed to the create_paths() method, view functions/methods and
their corresponding url routing rules are extracted from ths object. These url rules are then converted into path parameter metadata for
the generated paths of the output OpenAPI specification. Using Flask, for example:
@app.get("/users/<int:user_id>/pets/<pet_name>")
def get_user_pet_by_name(user_id: int, pet_name: str):
...
spec.create_paths(app)
The above code will result in the following OpenAPI path object:
paths:
/users/{user_id}/pets/{pet_name}:
parameters:
- in: path
name: user_id
required: true
schema:
type: integer
- in: path
name: pet_name
required: true
schema:
type: string
Adding Request Body Metadata to Operations¶
As specargs is intended to provide a thin wrapper around webargs, it also provides
use_args() and use_kwargs() decorator functions. On top of the
functionality they provide in webargs, these decorators also attach metadata onto decorated view
functions/methods. This metadata can then be used by an instance of WebargsAPISpec to generate parameter metadata
in the resulting OpenAPI specification. These decorators can be used as shown below:
from flask import Flask
from specargs import use_args
from webargs import fields
app = Flask(__name__, static_folder=None)
@app.post("/users")
@use_args({"name": fields.String(), "age": fields.Integer()}) # Must come after Flask decorator
def post_user(args):
print(args["name"])
...
# If using class-based views, methods can be decorated instead
from flask.view import MethodView
class Users(MethodView):
@use_args({"name": fields.String(required=True), "age": fields.Integer()})
def post(args):
print(args["name"])
print(args.get("age"))
...
specargs.use_kwargs() is used the same way, but will pass in keyword arguments instead of a single
positional argument:
@app.post("/users")
@use_kwargs({"name": fields.String(required=True), "age": fields.Integer()})
def post_user(name: str, age: int = None):
print(name)
print(age)
...
The above code snippets will all result in the same OpenAPI structure:
paths:
/users:
get:
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
age:
type: integer
Adding Parameter Metadata to Operations¶
The same specargs.use_args() and specargs.use_kwargs() methods can be used to provide metadata
for parameters not accepted in the request body. For example:
@app.get("/users")
@use_args({"name": fields.String()}, location="query") # Default 'location' is the same as the webargs parser
def get_users(args):
print(args["name"])
...
The above code snippet will result in this OpenAPI structure:
paths:
/users:
get:
parameters:
- in: query
name: name
required: false
schema:
type: string
Adding Response Metadata¶
Building on use_args() and use_kwargs(), specargs provides another decorator
function use_response(), which attaches response metadata to view functions/methods for use by an
instance of specargs.WebargsAPISpec:
@dataclass
class User:
id: int
name: str
age: int
@app.get("/users/<int:user_id>")
@use_response(
{"id": fields.Integer(), "name": fields.String(), "age": fields.Integer()},
description="The requested user", # Default description is an empty string
)
def get_user(user_id: int):
...
@app.post("/users")
@use_kwargs({"name": fields.String(), "age": fields.Integer()})
@use_response(
fields.String, # Can also be provided as `fields.String(kwargs**)` if using non-default kwargs
status_code=HTTPStatus.CREATED, # Default status_code is HTTPStatus.OK (200)
)
def post_user(name: str, age: int):
...
This will result in the following OAS structure:
paths:
/users:
post:
responses:
201:
description: ""
content:
text/html:
schema:
type: string
/users/{user_id}:
parameters:
- in: path
name: user_id
required: true
schema:
type: integer
get:
responses:
200:
description: The requested user
content:
application/json:
schema:
type: object
properties:
id:
schema:
type: integer
name:
schema:
type: string
age:
schema:
type: integer
Aside from marshmallow.fields and dictionaries of marshmallow.fields as shown in the example above,
use_response() can also accept a marshmallow.Schema class or instance (Schemas), a
specargs.in_poly.InPoly object (Schema Inheritance and Polymorphism), or a
specargs.Response (Responses) as its first argument. This argument determines the contents
of the content block in the generated OAS structure.
Adding Empty Responses¶
specargs also provides the convenience decorator use_empty_response() for cases like an empty 404
response:
@app.get("/users/<int:user_id>")
@use_empty_response(status_code=HTTPStatus.NOT_FOUND, description="The requested user was not found")
def get_user(user_id: int):
if user_id == NON_EXISTENT_USER_ID:
abort(404)
return User(id=user_id, name="Joe", age=24)
This would result in the same OAS output as if use_response() were provided an empty dictionary or
None as the first argument:
paths:
/users/{user_id}:
parameters:
- in: path
name: user_id
required: true
type: integer
get:
responses:
400:
description: The requested user was not found
Response Data Serialization¶
While use_args() and use_kwargs() provide request data parsing,
use_response() provides response data serialization based on marshmallow. In
the code example shown in Adding Response Metadata, a Flask view function returns a User object, but because
it’s decorated with use_response(), the User object is serialized into a dictionary and placed into a
tuple, which is an acceptable return value for Flask. The underlying implementation of this serialization is dynamic so
that the serialized output is in a form that’s appropriate for the current Active Framework.
Note
use_empty_response() will not serialize view function/method return data as no serialization schema is
provided.
Adding Extra Responses with Content¶
There may be times when a view function/method may need to explicitly return more than one kind of response with
differing content and status codes. In this case, the view function/method can be decorated with multiple
use_response() decorators, but as mentioned in Response Data Serialization, this would affect
the serialization of the return value depending on which response schema is used:
@app.post("/users/{user_id}")
@use_response(
{"id": fields.Integer(), "name": fields.String(), "age": fields.Integer()},
description="The requested user"
)
@use_response(
fields.String(),
description="The requested user was not found",
status_code=HTTPStatus.NOT_FOUND
)
def get_user(user_id: int):
if user_id == NON_EXISTENT_USER_ID:
# Needs to be handled by the second `use_response` above
return "The requested user was not found!", HTTPStatus.NOT_FOUND
# Should be handled by the first `use_response` above
return User(id=user_id, name="Joe", age=24)
By default, the return data of a view function/method will be processed by the topmost decorator. In the example above,
this means the first use_response() decorator would be used to serialize the data from both of the
return statements. In order to specify which decorator should process the return data, specargs provides the
ViewResponse class. The ViewResponse constructor accepts the return data as its first
argument and the intended response status as its second argument. The return data will then be processed by whichever
decorator has a matching status_code:
from specargs import use_response, use_empty_response, ViewResponse
@app.post("/users/{user_id}")
@use_response(
{"id": fields.Integer(), "name": fields.String(), "age": fields.Integer()},
description="The requested user"
)
@use_response(
fields.String(),
description="The requested user was not found",
status_code=HTTPStatus.NOT_FOUND
)
def get_user(user_id: int):
if user_id == NON_EXISTENT_USER_ID:
# Will now be handled by the second `use_response` decorator
return ViewResponse("The requested user was not found!", HTTPStatus.ACCEPTED)
# Will still be handled by the default first `use_response` decorator
return User(id=user_id, name="Joe", age=24)
Reusable Components¶
In OAS, certian objects (schemas, responses, etc.) are able to be defined in the top level components section of an OAS file. These defined components can then be referenced within other parts of the file to avoid repetition. specargs provides means to do the same within code.
Schemas¶
marshmallow provides an analog to OAS schema objects wwith their Schema
class. marshmallow Schema objects are accepted by both
use_args() and use_kwargs(), just like in webargs. However, simply
defining and using them in those decorators won’t add them to the components section of the generated OAS file. In
order to properly register a reusable schema in the OAS file, the corresponding Schema must be
provided to the schema() method of the specargs.WebargsAPISpec class. After
being defined, a Schema class or instance can be provided use_args(),
use_kwargs(), or use_response() which will provide request parsing and response data
serialization for the decorated view function/method.
from marshmallow import Schema, fields, validate
from specargs import WebargsAPISpec
spec = WebargsAPISpec(...)
@spec.schema
class NewUserSchema(Schema):
name = fields.String(required=True)
age = fields.Integer(validator=validate.Range(min=1, max=200))
@spec.schema("User")
class ExistingUserSchema(Schema):
id = fields.Integer(required=True)
name = fields.String(required=True)
age = fields.Integer(validator=validate.Range(min=1, max=200))
@dataclass
class User:
id: int
name: str
age: int
@app.post("/users")
@use_kwargs(NewUserSchema)
@use_response(ExistingUserSchema, description="The newly created user", status_code=HTTPStatus.CREATED)
def post_user(name: str, age: int):
return User(1, "Joe", 25)
The above code will result in the following OAS output:
components:
schemas:
NewUser:
type: object
properties:
name:
type: string
age:
type: integer
minimum: 1
maximum: 200
required:
- name
User:
type: object
properties:
id:
type: integer
name:
type: string
age:
type: integer
minimum: 1
maximum: 200
required:
- id
- name
paths:
/users:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/NewUser'
required: true
responses:
'201':
description: The newly created user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
Responses¶
specargs provides the Response class to generate reusable response components. Instead of
defining response metadata directly within the use_response() decorator, this metatdata can be defined
within the Response constructor. The resulting Response object can then be
provided to multiple use_response() decorators, reducing repetition when defining view functions/methods
with the same response metadata. However, instantiating a Response object with its constructor does
not automatically register it as a reusable response component. To accomplish this, the Response
instance can be provided to the response() method of the
WebargsAPISpec class, which will register a corresponding response object in the components section
of the generated OAS output:
from marshmallow import Schema, fields
from specargs import WebargsAPISpec, Response
spec = WebargsAPISpec(...)
class UserSchema(Schema):
id = fields.Integer()
name = fields.String()
age = fields.Integer()
user_response = Response(UserSchema, description="A user")
# The first argument is the desired name of the response object within the OAS output
spec.response("UserResponse", user_response)
Alternatively, it’s possible to combine the steps of construction and registration by using the
specargs.WebargsAPISpec.response() method as a Response factory. After its first argument,
response() is able to accept any arguments and keyword arguments that would be provided to
the Response constructor:
# Importing 'Response' from 'specargs' is no longer needed
user_response = spec.response("UserResponse", UserSchema, description="A user")
Once a Response object is created, it can then be provided to the use_response()
decorator:
# After a `Response` object named `user_response` has been created
@app.get("/users/<int:user_id>")
@use_response(user_response)
def get_user(user_id: int):
...
@app.post("/users")
@use_kwargs({"name": fields.String(), "age": fields.Integer()})
@user_response(user_response, status_code=HTTPStatus.CREATED)
def post_user(name: str, age: int):
...
The resulting OAS output would be:
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
age:
type: integer
responses:
UserResponse:
description: A user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
paths:
/users:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
age:
type: integer
required: true
responses:
'201':
$ref: '#/components/responses/UserResponse'
/users/{user_id}:
parameters:
- in: path
name: user_id
required: true
schema:
type: integer
get:
respones:
'200':
$ref: '#/components/responses/UserResponse'
Schema Inheritance and Polymorphism¶
In OAS, schema inheritance and polymorphism is accomplished using the oneOf, anyOf, and allOf keywords. In order
to match the features of OAS, specargs provides the OneOf, AnyOf, and
AllOf classes which all inherit from the specargs.in_poly.InPoly class. These classes can
be used in all specargs functions and methods where a dictionary of marshmallow.fields or a
marshmallow.Schema class or instance can be provided. Before the explanation and examples of how each of these
InPoly subclasses can be used, let the following dataclasses and schemas be defined:
from dataclass import dataclass
from marshmallow import Schema, fields
from specargs import WebargsAPISpec
spec = WebargsAPISpec(...)
@dataclass
class Spoon:
volume: float
@dataclass
class Fork:
prongs: int
@dataclass
class Spork:
volume: float
prongs: int
@dataclass
class Knife:
serrated: bool
@spec.schema
class SpoonSchema(Schema):
volume = fields.Float()
@spec.schema
class ForkSchema(Schema):
prongs = fields.Integer()
The schemas defined above will generate the following OAS components:
components:
schemas:
Spoon:
type: object
properties:
volume:
type: number
Fork:
type: object
properties:
prongs:
type: integer
OneOf¶
The OneOf class can be used to define request/response metadata with multiple object schemas, only
one of which should successfully validate the given data. The schemas can be provided as marshmallow.Schema
classes or instances:
from specargs import use_args, use_response, use_empty_response, OneOf
@app.post("/utensils")
@use_args(OneOf(SpoonSchema(), ForkSchema))
@use_empty_response(description="A new utensil was successfully posted", status_code=HTTPStatus.CREATED)
def post_utensil(args: dict):
# At this point we have a dictionary that either has the attributes of Spoon or the attributes of Fork
# It may be useful to add `post_load` methods to both Schemas so that the resulting `args` object is a Fork or
# Spoon instead of a dictionary
...
@app.get("/utensils/<int:utensil_id>")
@use_response(OneOf(SpoonSchema, ForkSchema()), description="The requested utensil")
def get_utensil(utensil_id: int):
... # Steps that lead to returning a Spoon
return Spoon(volume=14.8) # Valid against only SpoonSchema (CORRECT)
... # Steps that lead to returning a Fork
return Fork(prongs=3) # Valid against only ForkSchema (CORRECT)
... # Steps that lead to returning a Spork
return Spork(volume=13.2, prongs=4) # Valid against both schemas (ERROR)
... # Steps that lead to returning a Knife
return Knife(serrated=True) # Valid against neither schema (ERROR)
When parsing request data or serializing response data, using OneOf will raise an error if the data
is valid for more than one of the provided schemas or if the data is invalid against all provided schemas. The above
code will result in the following OAS output:
paths:
/utensils:
post:
requestBody:
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Spoon'
- $ref: '#/components/schemas/Fork'
responses:
'201':
description: A new utensil was successfully posted
/utensils/{utensil_id}:
parameters:
- in: path
name: utensil_id
required: true
schema:
type: integer
get:
responses:
'200':
description: The requested utensil
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Spoon'
- $ref: '#/components/schemas/Fork'
AnyOf¶
The AnyOf class can be used to define request/response metadata with multiple object schemas,
multiple of which could successfully validate the given data. The schemas can be provided as marshmallow.Schema
classes or instances:
from specargs import use_args, use_response, use_empty_response, OneOf
@app.post("/utensils")
@use_args(AnyOf(SpoonSchema(), ForkSchema))
@use_empty_response(description="A new utensil was successfully posted", status_code=HTTPStatus.CREATED)
def post_utensil(args: dict):
# At this point we have a dictionary that has either the attributes of Spoon, the attributes of Fork, or both
# Schemas with `post_load` methods that return non-dictionary objects will still output a dictionary
...
@app.get("/utensils/<int:utensil_id>")
@use_response(AnyOf(SpoonSchema, ForkSchema()), description="The requested utensil")
def get_utensil(utensil_id: int):
... # Steps that lead to returning a Spoon
return Spoon(volume=14.8) # Valid against only SpoonSchema (CORRECT)
... # Steps that lead to returning a Fork
return Fork(prongs=3) # Valid against only ForkSchema (CORRECT)
... # Steps that lead to returning a Spork
return Spork(volume=13.2, prongs=4) # Valid against both schemas (CORRECT)
... # Steps that lead to returning a Knife
return Knife(serrated=True) # Valid against neither schema (ERROR)
When parsing request data or serializing response data, using AnyOf will raise an error if the data
is invalid against all of the provided schemas. The above code will result in the following OAS output:
paths:
/utensils:
post:
requestBody:
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/Spoon'
- $ref: '#/components/schemas/Fork'
responses:
'201':
description: A new utensil was successfully posted
/utensils/{utensil_id}:
parameters:
- in: path
name: utensil_id
required: true
schema:
type: integer
get:
responses:
'200':
description: The requested utensil
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/Spoon'
- $ref: '#/components/schemas/Fork'
Using AnyOf for request parsing and response serialization will also raise an error if the provided
schemas result in differing values for any given key after parsing or serialization. For example, cases in which the
provided schemas contain fields with matching names but differing types will raise an error.
AllOf¶
The AllOf class can be used to define request/response metadata with multiple object schemas, all of
which should successfully validate the given data. The schemas can be provided as marshmallow.Schema classes or
instances:
from specargs import use_args, use_response, use_empty_response, OneOf
@app.post("/utensils")
@use_args(AllOf(SpoonSchema(), ForkSchema))
@use_empty_response(description="A new utensil was successfully posted", status_code=HTTPStatus.CREATED)
def post_utensil(args: dict):
# At this point we have a dictionary that has the attributes of Spoon and the attributes of Fork
# Schemas with `post_load` methods that return non-dictionary objects will still output a dictionary
...
@app.get("/utensils/<int:utensil_id>")
@use_response(AnyOf(SpoonSchema, ForkSchema()), description="The requested utensil")
def get_utensil(utensil_id: int):
... # Steps that lead to returning a Spoon
return Spoon(volume=14.8) # Valid against only SpoonSchema (ERROR)
... # Steps that lead to returning a Fork
return Fork(prongs=3) # Valid against only ForkSchema (ERROR)
... # Steps that lead to returning a Spork
return Spork(volume=13.2, prongs=4) # Valid against both schemas (CORRECT)
... # Steps that lead to returning a Knife
return Knife(serrated=True) # Valid against neither schema (ERROR)
When parsing request data or serializing response data, using AllOf will raise an error if the data
is invalid against any of the provided schemas. The above code will result in the following OAS output:
paths:
/utensils:
post:
requestBody:
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Spoon'
- $ref: '#/components/schemas/Fork'
responses:
'201':
description: A new utensil was successfully posted
/utensils/{utensil_id}:
parameters:
- in: path
name: utensil_id
required: true
schema:
type: integer
get:
responses:
'200':
description: The requested utensil
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Spoon'
- $ref: '#/components/schemas/Fork'
Using AllOf for request parsing and response serialization will also raise an error if the provided
schemas result in differing values for any given key after parsing or serialization. For example, cases in which the
provided schemas contain fields with matching names but differing types will raise an error.
Generating an OAS File¶
Once all components have been added to a WebargsAPISpec instance, an OAS definition can be
output using the to_dict() and to_yaml()
methods, exactly as with apispec.APISpec.