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:
Verify that you’re using the correct placeholder syntax: {constraint}, {value} and {field}.
Check that the rule name in your message_mapping matches the rule in your schema
Translation Issues
If translations aren’t working as expected:
Make sure your translator function is callable and returns a string
Verify your .po/.mo files are properly formatted and located
Test your translator function independently to confirm it works
Remember that placeholders are replaced before translation occurs
Complex Validation Rules
For complex validations with custom rules:
Define custom error messages that include {constraint} placeholders
Use the {field} placeholder to indicate which field failed validation
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc090472284741c6f0f84d3041867357bf6075511528ff7c995c8efa07715a58
|
|
| MD5 |
4872f59f3252f31129d6ee01d00a3415
|
|
| BLAKE2b-256 |
17d44153693ec6ff5c2e394b828cd5c2011f51c973491e49f9fd4e16ce90cba8
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9585269dea78af669ccf9579351046019e2018dd1fcf4bbf1361bd3ba4b17c57
|
|
| MD5 |
557ff96fe4f05dfcd770eb58d8ac6a86
|
|
| BLAKE2b-256 |
d248514511ffe4c7a829baae073ad181ea502ffc510ab3706787958fcfcb35f0
|