Skip to main content

Cerberus validation support for Morepath

Project description

more.cerberus: validation and normalization support for Morepath

This package provides Morepath integration for the Cerberus data validation library:

Cerberus can automate user input validation and normalization in a HTTP API. We also support custom error messages and translations for multi-language applications.

Schema

You can define a schema simply as a Python dict:

user_schema = {
  'name': {'type': 'string', 'minlength' : 3, 'required': True},
  'age': {'type': 'integer', 'min': 0, 'required': True}
}

Alternatively you can define the schema in yaml and load it with pyyaml:

user:
  name:
    type: string
    minlength: 3
    required: true
  age:
    type: integer
    min: 0
    required: true
import yaml

with open('schema.yml') as schema:
    schema = yaml.load(schema)

user_schema = schema['user']

Validate

The more.cerberus integration helps with validation of the request body as it is POSTed or PUT to a view. First we must create a loader for our schema:

from more.cerberus import loader

user_schema_load = loader(user_schema)

We can use this loader to handle a PUT or POST request for instance:

@App.json(model=User, request_method='POST', load=user_schema_load)
def user_post(self, request, json):
    # json is now a validated and normalized dict of whatever got
    # POST onto this view that you can use to update
    # self

Update models

By default in PUT or PATCH requests the load function sets the update flag of the validate() method to True, so required fields won’t be checked. For other requests like POST update is False.

You can set this manually by passing the update argument to the load function:

user_schema_load = loader(user_schema, update=False)

@App.json(model=User, request_method='PUT', load=user_schema_load)
def user_put(self, request, json):

Customize the Validator

With Cerberus you can customize the rules, data types, validators, coercers (for normalization) and default setters by subclassing CerberusValidator:

import re
from more.cerberus import CerberusValidator

class CustomValidator(CerberusValidator):
    def _check_with_validate_email(self, field, value):
      match = re.match(
        '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$',value
      )
      if match == None:
        self._error(field, 'Not valid email')

    def _normalize_coerce_normalize_email(self, value):
        parts = value.split('@')
        if len(parts) != 2:
          return value
        else:
          domain = parts[1].lower
          if domain == 'googlemail.com':
            domain = 'gmail.com'
          return parts[0] + '@' + domain

You have to pass the custom Validator class to the load function:

user_schema_load = loader(user_schema, validator=CustomValidator)

Now you can use the new email validator and normalizer in your schema:

user_schema = {
  'name': {
    'type': 'string',
    'minlength' : 3,
    'required': True,
  },
  'email': {
    'type': 'string',
    'check_with': 'validate_email',
    'coerce': 'normalize_email',
    'required': True,
  }
}

or with YAML:

user:
  name:
    type: string
    minlength: 3
    required: true
  email:
    type: string
    check_with: validate_email
    coerce: normalize_email
    required: true

For more information how to customize the Validator take a look at the Cerberus documentation.

Use the request or app instance in your custom validator

In CerberusValidator you can access the request through self.request and the app through self.request.app. Like this you can use e.g. Morepath settings and services when extending rules.

Here an example from auth-boilerplate for custom email validation and normalization using a service based on email_validator:

from more.cerberus import CerberusValidator
from email_validator import EmailSyntaxError, EmailUndeliverableError


class Validator(CerberusValidator):
    def _check_with_verify_email(self, field, value):
        email_validation_service = self.request.app.service(
            name='email_validation'
        )
        try:
            email_validation_service.verify(value)

        except EmailSyntaxError:
            self._error(field, 'Not valid email')

        except EmailUndeliverableError:
            self._error(field, 'Email could not be delivered')

    def _normalize_coerce_normalize_email(self, value):
        email_validation_service = self.request.app.service(
            name='email_validation'
        )
        return email_validation_service.normalize(value)

Custom Error Messages and Translation

You can customize error messages and translate them using Cerberus integration in more.cerberus. This is useful for multi-language applications and for providing more user-friendly validation errors.

Basic Usage

You can customize error messages with placeholders:

from more.cerberus import loader

# Define your schema
schema = {
    "name": {"type": "string", "minlength": 3, "required": True},
    "age": {"type": "integer", "min": 18, "required": True},
}

# Define custom messages
messages = {
    "required": "This field is mandatory",
    "minlength": "Must be at least {minlength} characters",
    "min": "Must be at least {min}"
}

# Create your validator without translation
validator = loader(schema, message_mapping=messages)

# With a translation function using Python's standard gettext
import gettext
translations = gettext.translation('myapp', 'locale', languages=['de'])
_ = translations.gettext
validator_i18n = loader(schema, translator_func=_, message_mapping=messages)

Always use curly braces {} for placeholders in your custom error messages. Supported placeholders include:

  • {constraint}: The general validation constraint value

    (compatible with all rules)

  • Rule-specific placeholders matching the rule names:

    • {min}: For min value validation

    • {max}: For max value validation

    • {minlength}: For minimum string length

    • {maxlength}: For maximum string length

    • {type}: For type validation (also handles complex types like “string or integer”)

  • {field}: The field name being validated

  • {value}: The value that failed validation

Placeholders will be automatically replaced with their actual values during validation. You can use either {constraint} or rule-specific placeholders in your message templates.

Translation Integration

The translation functionality is designed to work with any gettext-based translation system:

  • Python’s built-in gettext module

  • Babel-based translation systems

  • Any custom translation function that takes a string and returns a translated string

You can also specify a translation domain when initializing the loader:

# Specify a custom translation domain (default is "messages")
translations = gettext.translation('my_domain', 'locale', languages=['de'])
_ = translations.gettext
validator = loader(schema, translator_func=_)

The translation_domain parameter helps organize translations into separate catalogs in gettext-based translation systems. This allows you to keep validation error messages in their own namespace, separate from other application translations.

Message Mapping

You can define custom messages for any Cerberus validation rule. Use the same rule names as in your schema:

  • required: For required fields

  • minlength: For string minimum length (with {minlength} placeholder)

  • maxlength: For string maximum length (with {maxlength} placeholder)

  • type: For type validation errors (with {type} or {constraint} placeholder)

  • min: For minimum numeric values (with {min} or {constraint} placeholder)

  • max: For maximum numeric values (with {max} or {constraint} placeholder)

  • regex: For regular expression validation errors

  • … and others from Cerberus

You can use either specific rule name placeholders (like {min}) or the general {constraint} placeholder which is part of Cerberus’ built-in error system.

Here’s how placeholders are substituted at runtime:

# Schema definition
schema = {"age": {"type": "integer", "min": 18}}
message_mapping = {"min": "Value must be at least {min}"}
# or using constraint: message_mapping = {"min": "Value must be at least {constraint}"}

# What users will see when they enter "10" as age:
# "Value must be at least 18"

Multiple Type Validation

The library handles complex type validations elegantly. For example, when a field can accept multiple types:

# Schema accepting either string or integer
schema = {"id": {"type": ["string", "integer"]}}
message_mapping = {"type": "Must be either {type}"}

# What users will see with invalid input:
# "Must be either string or integer"

YAML Message Mapping

You can also organize message mappings hierarchically using YAML files. For example:

# messages.yml
required: This field is mandatory
min: Value must be at least {min}
max: Value must not exceed {max}
type: Field must be of {type} type
minlength: Must be at least {minlength} characters
maxlength: Cannot exceed {maxlength} characters
regex: Invalid format

Then load them in your Python code:

import yaml

with open('messages.yml') as f:
    message_mapping = yaml.safe_load(f)

validator = loader(schema, message_mapping=message_mapping)
  • Define global default messages in a central location (e.g., settings/default_messages.yml)

  • Override specific messages with module-level files (e.g., users/messages.yml)

  • Load and merge these mappings before passing them to the validator

Translation Extraction

When using YAML files for message definitions, you’ll need a way to extract those strings for translation. A recommended approach is to create a script that reads all your YAML message files and generates a Python file with the messages wrapped in translation markers.

Here’s a simple example of such a script:

#!/usr/bin/env python
import yaml
import sys
from pathlib import Path

# Find message files in your project
source_dir = Path("src")
yaml_files = list(source_dir.glob("**/*messages.yml"))

# Write header to output file
output_file = open("translations/validation_messages.py", "w")
print("# Generated translation markers", file=output_file)
print("def _(text): return text\n", file=output_file)

# Process each YAML file
for yaml_file in yaml_files:
    print(f"# From {yaml_file}:", file=output_file)
    with open(yaml_file) as f:
        messages = yaml.safe_load(f) or {}

    # Extract messages for translation
    for key, message in messages.items():
        if message and isinstance(message, str):
            print(f'_("{message}")  # {key}', file=output_file)

This generated file can then be processed with standard translation tools like Babel/pybabel to create .po files, which you would then translate and compile into .mo files following your regular translation workflow.

Example in a Morepath App

import gettext
from more.cerberus import CerberusApp, loader
from morepath import redirect

class App(CerberusApp):
    pass

user_schema = {
    "name": {"type": "string", "required": True},
    "email": {"type": "string", "required": True}
}

messages = {
    "required": "This field is required.",
    "type": "Must be of {constraint} type"
}

# Set up translations for the view
translations = gettext.translation('messages', 'locale', languages=['de'])
_ = translations.gettext

@App.json(
  model=User,
  request_method="POST",
  load=loader(user_schema, translator_func=_, message_mapping=messages)
)
def create_user(self, request, json):
    # Handle validated input

Troubleshooting

Here are solutions to common issues you might encounter:

Missing Placeholders

If placeholders in your error messages aren’t being replaced:

  1. Verify that you’re using the correct placeholder syntax: {constraint}, {value} and {field}.

  2. Check that the rule name in your message_mapping matches the rule in your schema

Translation Issues

If translations aren’t working as expected:

  1. Make sure your translator function is callable and returns a string

  2. Verify your .po/.mo files are properly formatted and located

  3. Test your translator function independently to confirm it works

  4. Remember that placeholders are replaced before translation occurs

Complex Validation Rules

For complex validations with custom rules:

  1. Define custom error messages that include {constraint} placeholders

  2. Use the {field} placeholder to indicate which field failed validation

  3. Consider using the {value} placeholder to show the invalid input in error messages

Error handling

If validation fails due to a validation error (a required field is missing, or a field is of the wrong datatype, for instance), you want to show some kind of error message. The load function created by more.cerberus raises the more.cerberus.ValidationError exception in case of errors.

This exception object has an errors attribute with the validation errors. You must define an exception view for it, otherwise validation errors are returned as “500 internal server error” to API users.

This package provides a default exception view implementation. If you subclass your application from more.cerberus.CerberusApp then you get a default error view for ValidationError that has a 422 status code with a JSON response with the Cerberus errors structure:

from more.cerberus import CerberusApp

class App(CerberusApp):
    pass

Now your app has reasonable error handling built-in.

If you want a different error view you can instead create it by yourself, e.g.:

from more.cerberus.error import ValidationError

from .app import App


@App.json(model=ValidationError)
def validation_error(self, request):
    @request.after
    def set_status(response):
        response.status = 422

    errors = list(self.errors.values())[0][0]

    return {
        'errors': errors
    }

This could be used to extract the errors from a schema wrapped into a dictionary like:

article-schema:
  article:
    type: dict
    schema:
      title:
        type: string
        required: true
      body:
        type: string
        required: true

CHANGES

0.4 (2025-06-12)

  • Add support for custom error messages with placeholders {constraint}, {field} and {value}. When error.info tuple is available, you can also use positional placeholders ({0}, {1}) in the error messages.

  • Add translation support for error messages.

  • Drop support for Python 3.4 - 3.7.

  • Add support for Python 3.9 - 3.12.

  • Use GitHub Actions for CI.

0.3 (2020-04-26)

  • Removed: Removed support for Python 2.

    You have to upgrade to Python 3 if you want to use this version.

  • Added support for Python 3.7 and 3.8 and PyPy 3.6.

  • Make Python 3.7 the default testing environment.

  • Upgrade Cerberus to version 1.3.2.

  • Add integration for the Black code formatter.

0.2 (2018-02-11)

  • Add Python 3.6 support.

  • Add example for creating a custom error view to README.

  • Some smaller fixes.

0.1 (2017-03-17)

  • initial public release.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

more.cerberus-0.4.tar.gz (24.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

more.cerberus-0.4-py2.py3-none-any.whl (21.5 kB view details)

Uploaded Python 2Python 3

File details

Details for the file more.cerberus-0.4.tar.gz.

File metadata

  • Download URL: more.cerberus-0.4.tar.gz
  • Upload date:
  • Size: 24.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.0 CPython/3.11.2

File hashes

Hashes for more.cerberus-0.4.tar.gz
Algorithm Hash digest
SHA256 fc090472284741c6f0f84d3041867357bf6075511528ff7c995c8efa07715a58
MD5 4872f59f3252f31129d6ee01d00a3415
BLAKE2b-256 17d44153693ec6ff5c2e394b828cd5c2011f51c973491e49f9fd4e16ce90cba8

See more details on using hashes here.

File details

Details for the file more.cerberus-0.4-py2.py3-none-any.whl.

File metadata

  • Download URL: more.cerberus-0.4-py2.py3-none-any.whl
  • Upload date:
  • Size: 21.5 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.0 CPython/3.11.2

File hashes

Hashes for more.cerberus-0.4-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 9585269dea78af669ccf9579351046019e2018dd1fcf4bbf1361bd3ba4b17c57
MD5 557ff96fe4f05dfcd770eb58d8ac6a86
BLAKE2b-256 d248514511ffe4c7a829baae073ad181ea502ffc510ab3706787958fcfcb35f0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page