Skip to content
A query parameters validation library
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.

Qval | Query params validation library

CircleCI Documentation Status codecov PyPI version Code style: black


$ pip install qval

Basic usage

You can use Qval both as a function and as a decorator. The function validate() accepts 3 positional arguments and 1 named:

def validate(
    # Request instance. Must be a dictionary or implement request interface.
    request: Union[Request, Dict[str, str]],
    # Dictionary of (param_name -> `Validator()` object).
    validators: Dict[str, Validator] = None,
    # Provide true if you want to access all parameters of the request in the context.
    box_all: bool = True,
    # Factories that will be used to convert parameters to python objects (param -> [callable[str] => object]).
) -> QueryParamValidator: 

Imagine you have a RESTful calculator with an endpoint called /api/divide. You can use validate() to automatically convert parameters to python objects and then validate them:

from qval import validate

def division_view(request):
    GET /api/divide?
    param a     : int
    param b     : int, nonzero
    param token : string, length = 12
    Example: GET /api/divide?a=10&b=2&token=abcdefghijkl -> 200, {"answer": 5}
    # Parameter validation occurs in the context manager.
    # If validation fails or user code throws an error, context manager
    # will raise InvalidQueryParamException or APIException respectively.
    # In Django Rest Framework, these exceptions will be processed and result 
    # in error codes (400 and 500) on the client side.
    params = (
        # `a` and `b` must be integers
        # Note: in order to get a nice error message on the client side,
        # you factory should raise either ValueError or TypeError
        validate(request, a=int, b=int)
        # `b` must be anything but zero
        # The `transform` callable will be applied to parameter before the check.
        # In this case we'll get `token`'s length and check if it is equal to 12.
        .eq("token", 12, transform=len)
    # validation starts here
    with params as p:
        return Response({"answer": p.a // p.b})
// GET /api/divide?a=10&b=2&token=abcdefghijkl
// Browser:
  "answer": 5

Sending b = 0 to this endpoint will result in the following message on the client side:

// GET /api/divide?a=10&b=0&token=abcdefghijkl
  "error": "Invalid `b` value: 0."

If you have many parameters and custom validators, it's better to use the @qval() decorator:

from decimal import Decimal
from qval import Validator, QvalValidationError

def price_validator(price: int) -> bool:
    A predicate to validate `price` query parameter.
    Provides custom error message.
    if price <= 0:
        # If price does not match our requirements, we raise QvalValidationError() with a custom message.
        # This exception will be handled in the context manager and will be reraised
        # as InvalidQueryParamException() [HTTP 400].
        raise QvalValidationError(f"Price must be greater than zero, got \'{price}\'.")
    return True

purchase_factories = {"price": Decimal, "item_id": int, "token": None}
purchase_validators = {
    "token": Validator(lambda x: len(x) == 12),
    # Validator(p) can be omitted if there is only one predicate:
    "item_id": lambda x: x >= 0,
    "price": price_validator,

from qval import qval
from validators import *

# Any function or method wrapped with `qval()` must accept request as 
# either first or second argument, and parameters as last.
@qval(purchase_factories, purchase_validators)
def purchase_view(request, params):
    GET /api/purchase?
    param item_id : int, positive
    param price   : float, greater than zero
    param token   : string, len == 12

    Example: GET /api/purchase?item_id=1&price=5.8&token=abcdefghijkl
    print(f"{params.item_id} costs {params.price}$.")

Framework-specific instructions

  1. Django Rest Framework works straight out of the box. Simply add @qval() to your views or use validate() inside.

  2. For Django without DRF you may need to add the exception handler to settings.MIDDLEWARE. Qval attempts to do it automatically if DJANO_SETTINGS_MODULE is set. Otherwise you'll see the following message:

    WARNING:root:Unable to add APIException middleware to the MIDDLEWARE list. Django does not 
    support APIException handling without DRF integration. Define DJANGO_SETTINGS_MODULE or 
    add 'qval.framework_integration.HandleAPIExceptionDjango' to the MIDDLEWARE list.

    Take a look at the plain Django example here.

  3. If you are using Flask, you will need to setup exception handlers:

    from flask import Flask
    from qval.framework_integration import setup_flask_error_handlers
    app = Flask(__name__)

    Since request in Flask is a global object, you may want to curry @qval() before usage:

    from flask import request
    from qval import qval_curry
    # Firstly, curry `qval()`
    qval = qval_curry(request)
    # Then use it as a decorator.
    # Note: you view now must accept request as first argument
    def view(request, params): 

    Check out the full Flask example in examples/

    You can run the example using the command below:

    $ PYTHONPATH=. FLASK_APP=examples/ flask run
  4. Similarly to Flask, with Falcon you will need to setup error handlers:

    import falcon
    from qval.framework_integration import setup_falcon_error_handlers
    app = falcon.API()

    Full Falcon example can be found here: examples/

    Use the following command to run the app:

    $ PYTHONPATH=. python examples/


Refer to documentation for more verbose descriptions and auto-generated API docs. You can also look at the tests to get the idea how the stuff below works.


Qval supports configuration via config files and environmental variables. If DJANGO_SETTINGS_MODULE or SETTINGS_MODULE are defined, the specified config module will be used. Otherwise, all lookups would be done in os.environ.

Supported variables:

  • QVAL_MAKE_REQUEST_WRAPPER = myapp.myfile.my_func. Customizes behaviour of the make_request() function, which is applied to all incoming requests, then the result is passed to qval.qval.QueryParamValidator. The provided function must accept request and return object that supports request interface (see qval.framework_integration.DummyReqiest).
    For example, the following code adds logging to each make_request() call:

    # app/
    def my_wrapper(f):
        def wrapper(request):
            print(f"Received new request: {request}")
            return f(request)
        return wrapper

    You also need to execute export QVAL_MAKE_REQUEST_WRAPPER=app.utils.my_wrapper in your console or to add it to the config file.

  • QVAL_REQUEST_CLASS = @qval() will use it to determine which argument is a request. If you have a custom request class that implements qval.framework_integration.DummyRequest interface, provide it with this variable.

  • QVAL_LOGGERS = [mylogger.factory, ...] | mylogger.factory. List of paths or a path to a factory callable. Specified callable must return object with the Logger interface. See section logging for more info.


Qval uses a global object called log acting as singleton when reporting errors. By default, logging.getLogger function is used as a factory on each call. You can provide your own factory (see configuration) or disable logging. Example error message:

An error occurred during the validation or inside of the context: exc `<class 'OverflowError'>` ((34, 'Numerical result out of range')).
| Parameters: <QueryDict: {'a': ['2.2324'], 'b': ['30000000']}>
| Body      : b''
| Exception:
Traceback (most recent call last):
  File "<path>/qval/", line 338, in inner
    return f(*args, params, **kwargs)
  File "<path>/examples/django-example/app/", line 46, in pow_view
    return JsonResponse({"answer": params.a ** params.b})
OverflowError: (34, 'Numerical result out of range')
Internal Server Error: /api/pow
[19/Nov/2018 07:03:15] "GET /api/pow?a=2.2324&b=30000000 HTTP/1.1" 500 102

Import the log object from qval and configure as you need:

from qval import log
# For instance, disable logging:
You can’t perform that action at this time.