Skip to content

Latest commit

 

History

History
422 lines (281 loc) · 13 KB

advanced.rst

File metadata and controls

422 lines (281 loc) · 13 KB

Advanced Usage

This section includes guides for advanced usage patterns.

Custom Location Handlers

To add your own custom location handler, write a function that receives a request, and a Schema <marshmallow.Schema>, then decorate that function with Parser.location_loader <webargs.core.Parser.location_loader>.

from webargs import fields
from webargs.flaskparser import parser


@parser.location_loader("data")
def load_data(request, schema):
    return request.data


# Now 'data' can be specified as a location
@parser.use_args({"per_page": fields.Int()}, location="data")
def posts(args):
    return "displaying {} posts".format(args["per_page"])

Note

The schema is passed so that it can be used to wrap multidict types and unpack List fields correctly. If you are writing a loader for a multidict type, consider looking at MultiDictProxy <webargs.multidictproxy.MultiDictProxy> for an example of how to do this.

"meta" Locations

You can define your own locations which mix data from several existing locations.

The json_or_form location does this -- first trying to load data as JSON and then falling back to a form body -- and its implementation is quite simple:

def load_json_or_form(self, req, schema):
    """Load data from a request, accepting either JSON or form-encoded
    data.

    The data will first be loaded as JSON, and, if that fails, it will be
    loaded as a form post.
    """
    data = self.load_json(req, schema)
    if data is not missing:
        return data
    return self.load_form(req, schema)

You can imagine your own locations with custom behaviors like this. For example, to mix query parameters and form body data, you might write the following:

from webargs import fields
from webargs.multidictproxy import MultiDictProxy
from webargs.flaskparser import parser


@parser.location_loader("query_and_form")
def load_data(request, schema):
    # relies on the Flask (werkzeug) MultiDict type's implementation of
    # these methods, but when you're extending webargs, you may know things
    # about your framework of choice
    newdata = request.args.copy()
    newdata.update(request.form)
    return MultiDictProxy(newdata, schema)


# Now 'query_and_form' means you can send these values in either location,
# and they will be *mixed* together into a new dict to pass to your schema
@parser.use_args({"favorite_food": fields.String()}, location="query_and_form")
def set_favorite_food(args):
    ...  # do stuff
    return "your favorite food is now set to {}".format(args["favorite_food"])

marshmallow Integration

When you need more flexibility in defining input schemas, you can pass a marshmallow Schema <marshmallow.Schema> instead of a dictionary to Parser.parse <webargs.core.Parser.parse>, Parser.use_args <webargs.core.Parser.use_args>, and Parser.use_kwargs <webargs.core.Parser.use_kwargs>.

from marshmallow import Schema, fields
from webargs.flaskparser import use_args


class UserSchema(Schema):
    id = fields.Int(dump_only=True)  # read-only (won't be parsed by webargs)
    username = fields.Str(required=True)
    password = fields.Str(load_only=True)  # write-only
    first_name = fields.Str(missing="")
    last_name = fields.Str(missing="")
    date_registered = fields.DateTime(dump_only=True)

    # NOTE: Uncomment below two lines if you're using marshmallow 2
    # class Meta:
    #    strict = True


@use_args(UserSchema())
def profile_view(args):
    username = args["userame"]
    # ...


@use_kwargs(UserSchema())
def profile_update(username, password, first_name, last_name):
    update_profile(username, password, first_name, last_name)
    # ...


# You can add additional parameters
@use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query")
@use_args(UserSchema())
def profile_posts(args, posts_per_page):
    username = args["username"]
    # ...

Warning

If you're using marshmallow 2, you should always set strict=True (either as a class Meta option or in the Schema's constructor) when passing a schema to webargs. This will ensure that the parser's error handler is invoked when expected.

When to avoid use_kwargs

Any Schema <marshmallow.Schema> passed to use_kwargs <webargs.core.Parser.use_kwargs> MUST deserialize to a dictionary of data. If your schema has a post_load <marshmallow.decorators.post_load> method that returns a non-dictionary, you should use use_args <webargs.core.Parser.use_args> instead.

from marshmallow import Schema, fields, post_load
from webargs.flaskparser import use_args


class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width


class RectangleSchema(Schema):
    length = fields.Float()
    width = fields.Float()

    @post_load
    def make_object(self, data, **kwargs):
        return Rectangle(**data)


@use_args(RectangleSchema)
def post(self, rect: Rectangle):
    return f"Area: {rect.length * rect.width}"

Packages such as marshmallow-sqlalchemy and marshmallow-dataclass generate schemas that deserialize to non-dictionary objects. Therefore, use_args <webargs.core.Parser.use_args> should be used with those schemas.

Schema Factories

If you need to parametrize a schema based on a given request, you can use a "Schema factory": a callable that receives the current request and returns a marshmallow.Schema instance.

Consider the following use cases:

  • Filtering via a query parameter by passing only to the Schema.
  • Handle partial updates for PATCH requests using marshmallow's partial loading API.
from flask import Flask
from marshmallow import Schema, fields
from webargs.flaskparser import use_args

app = Flask(__name__)


class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True)
    password = fields.Str(load_only=True)
    first_name = fields.Str(missing="")
    last_name = fields.Str(missing="")
    date_registered = fields.DateTime(dump_only=True)


def make_user_schema(request):
    # Filter based on 'fields' query parameter
    fields = request.args.get("fields", None)
    only = fields.split(",") if fields else None
    # Respect partial updates for PATCH requests
    partial = request.method == "PATCH"
    # Add current request to the schema's context
    return UserSchema(only=only, partial=partial, context={"request": request})


# Pass the factory to .parse, .use_args, or .use_kwargs
@app.route("/profile/", methods=["GET", "POST", "PATCH"])
@use_args(make_user_schema)
def profile_view(args):
    username = args.get("username")
    # ...

Reducing Boilerplate

We can reduce boilerplate and improve [re]usability with a simple helper function:

from webargs.flaskparser import use_args


def use_args_with(schema_cls, schema_kwargs=None, **kwargs):
    schema_kwargs = schema_kwargs or {}

    def factory(request):
        # Filter based on 'fields' query parameter
        only = request.args.get("fields", None)
        # Respect partial updates for PATCH requests
        partial = request.method == "PATCH"
        return schema_cls(
            only=only, partial=partial, context={"request": request}, **schema_kwargs
        )

    return use_args(factory, **kwargs)

Now we can attach input schemas to our view functions like so:

@use_args_with(UserSchema)
def profile_view(args):
    # ...
    get_profile(**args)

Custom Fields

See the "Custom Fields" section of the marshmallow docs for a detailed guide on defining custom fields which you can pass to webargs parsers: https://marshmallow.readthedocs.io/en/latest/custom_fields.html.

Using Method and Function Fields with webargs

Using the Method <marshmallow.fields.Method> and Function <marshmallow.fields.Function> fields requires that you pass the deserialize parameter.

@use_args({"cube": fields.Function(deserialize=lambda x: int(x) ** 3)})
def math_view(args):
    cube = args["cube"]
    # ...

Custom Parsers

To add your own parser, extend Parser <webargs.core.Parser> and implement the load_* method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments.

import re

from webargs import core
from webargs.flaskparser import FlaskParser


class NestedQueryFlaskParser(FlaskParser):
    """Parses nested query args

    This parser handles nested query args. It expects nested levels
    delimited by a period and then deserializes the query args into a
    nested dict.

    For example, the URL query params `?name.first=John&name.last=Boone`
    will yield the following dict:

        {
            'name': {
                'first': 'John',
                'last': 'Boone',
            }
        }
    """

    def load_querystring(self, req, schema):
        return _structure_dict(req.args)


def _structure_dict(dict_):
    def structure_dict_pair(r, key, value):
        m = re.match(r"(\w+)\.(.*)", key)
        if m:
            if r.get(m.group(1)) is None:
                r[m.group(1)] = {}
            structure_dict_pair(r[m.group(1)], m.group(2), value)
        else:
            r[key] = value

    r = {}
    for k, v in dict_.items():
        structure_dict_pair(r, k, v)
    return r

Returning HTTP 400 Responses

If you'd prefer validation errors to return status code 400 instead of 422, you can override DEFAULT_VALIDATION_STATUS on a Parser <webargs.core.Parser>.

from webargs.falconparser import FalconParser


class Parser(FalconParser):
    DEFAULT_VALIDATION_STATUS = 400


parser = Parser()
use_args = parser.use_args
use_kwargs = parser.use_kwargs

Bulk-type Arguments

In order to parse a JSON array of objects, pass many=True to your input Schema .

For example, you might implement JSON PATCH according to RFC 6902 like so:

from webargs import fields
from webargs.flaskparser import use_args
from marshmallow import Schema, validate


class PatchSchema(Schema):
    op = fields.Str(
        required=True,
        validate=validate.OneOf(["add", "remove", "replace", "move", "copy"]),
    )
    path = fields.Str(required=True)
    value = fields.Str(required=True)


@app.route("/profile/", methods=["patch"])
@use_args(PatchSchema(many=True))
def patch_blog(args):
    """Implements JSON Patch for the user profile

    Example JSON body:

    [
        {"op": "replace", "path": "/email", "value": "mynewemail@test.org"}
    ]
    """
    # ...

Mixing Locations

Arguments for different locations can be specified by passing location to each use_args <webargs.core.Parser.use_args> call:

# "json" is the default, used explicitly below
@app.route("/stacked", methods=["POST"])
@use_args({"page": fields.Int(), "q": fields.Str()}, location="query")
@use_args({"name": fields.Str()}, location="json")
def viewfunc(query_parsed, json_parsed):
    page = query_parsed["page"]
    name = json_parsed["name"]
    # ...

To reduce boilerplate, you could create shortcuts, like so:

import functools

query = functools.partial(use_args, location="query")
body = functools.partial(use_args, location="json")


@query({"page": fields.Int(), "q": fields.Int()})
@body({"name": fields.Str()})
def viewfunc(query_parsed, json_parsed):
    page = query_parsed["page"]
    name = json_parsed["name"]
    # ...

Next Steps

  • See the Framework Support <framework_support> page for framework-specific guides.
  • For example applications, check out the examples directory.