Schema
Schema validation just got Pythonic
Install / Use
/learn @keleshev/SchemaREADME
Schema validation just got Pythonic
schema is a library for validating Python data structures, such as those obtained from config-files, forms, external services or command-line parsing, converted from JSON/YAML (or something else) to Python data-types.
.. image:: https://secure.travis-ci.org/keleshev/schema.svg?branch=master :target: https://travis-ci.org/keleshev/schema
.. image:: https://img.shields.io/codecov/c/github/keleshev/schema.svg :target: http://codecov.io/github/keleshev/schema
Example
Here is a quick example to get a feeling of schema, validating a list of entries with personal information:
.. code:: python
from schema import Schema, And, Use, Optional, SchemaError
schema = Schema(
[
{
"name": And(str, len),
"age": And(Use(int), lambda n: 18 <= n <= 99),
Optional("gender"): And(
str,
Use(str.lower),
lambda s: s in ("squid", "kid"),
),
}
]
)
data = [
{"name": "Sue", "age": "28", "gender": "Squid"},
{"name": "Sam", "age": "42"},
{"name": "Sacha", "age": "20", "gender": "KID"},
]
validated = schema.validate(data)
assert validated == [
{"name": "Sue", "age": 28, "gender": "squid"},
{"name": "Sam", "age": 42},
{"name": "Sacha", "age": 20, "gender": "kid"},
]
If data is valid, Schema.validate will return the validated data
(optionally converted with Use calls, see below).
If data is invalid, Schema will raise SchemaError exception.
If you just want to check that the data is valid, schema.is_valid(data) will
return True or False.
Installation
Use pip <http://pip-installer.org>_ or easy_install::
pip install schema
Alternatively, you can just drop schema.py file into your project—it is
self-contained.
- schema is tested with Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 and PyPy.
- schema follows
semantic versioning <http://semver.org>_.
How Schema validates data
Types
If ``Schema(...)`` encounters a type (such as ``int``, ``str``, ``object``,
etc.), it will check if the corresponding piece of data is an instance of that type,
otherwise it will raise ``SchemaError``.
.. code:: python
>>> from schema import Schema
>>> Schema(int).validate(123)
123
>>> Schema(int).validate('123')
Traceback (most recent call last):
...
schema.SchemaUnexpectedTypeError: '123' should be instance of 'int'
>>> Schema(object).validate('hai')
'hai'
Callables
If Schema(...) encounters a callable (function, class, or object with
__call__ method) it will call it, and if its return value evaluates to
True it will continue validating, else—it will raise SchemaError.
.. code:: python
>>> import os
>>> Schema(os.path.exists).validate('./')
'./'
>>> Schema(os.path.exists).validate('./non-existent/')
Traceback (most recent call last):
...
schema.SchemaError: exists('./non-existent/') should evaluate to True
>>> Schema(lambda n: n > 0).validate(123)
123
>>> Schema(lambda n: n > 0).validate(-12)
Traceback (most recent call last):
...
schema.SchemaError: <lambda>(-12) should evaluate to True
"Validatables"
If ``Schema(...)`` encounters an object with method ``validate`` it will run
this method on corresponding data as ``data = obj.validate(data)``. This method
may raise ``SchemaError`` exception, which will tell ``Schema`` that that piece
of data is invalid, otherwise—it will continue validating.
An example of "validatable" is ``Regex``, that tries to match a string or a
buffer with the given regular expression (itself as a string, buffer or
compiled regex ``SRE_Pattern``):
.. code:: python
>>> from schema import Regex
>>> import re
>>> Regex(r'^foo').validate('foobar')
'foobar'
>>> Regex(r'^[A-Z]+$', flags=re.I).validate('those-dashes-dont-match')
Traceback (most recent call last):
...
schema.SchemaError: Regex('^[A-Z]+$', flags=re.IGNORECASE) does not match 'those-dashes-dont-match'
For a more general case, you can use ``Use`` for creating such objects.
``Use`` helps to use a function or type to convert a value while validating it:
.. code:: python
>>> from schema import Use
>>> Schema(Use(int)).validate('123')
123
>>> Schema(Use(lambda f: open(f, 'a'))).validate('LICENSE-MIT')
<_io.TextIOWrapper name='LICENSE-MIT' mode='a' encoding='UTF-8'>
Dropping the details, ``Use`` is basically:
.. code:: python
class Use(object):
def __init__(self, callable_):
self._callable = callable_
def validate(self, data):
try:
return self._callable(data)
except Exception as e:
raise SchemaError('%r raised %r' % (self._callable.__name__, e))
Sometimes you need to transform and validate part of data, but keep original data unchanged.
``Const`` helps to keep your data safe:
.. code:: python
>> from schema import Use, Const, And, Schema
>> from datetime import datetime
>> is_future = lambda date: datetime.now() > date
>> to_json = lambda v: {"timestamp": v}
>> Schema(And(Const(And(Use(datetime.fromtimestamp), is_future)), Use(to_json))).validate(1234567890)
{"timestamp": 1234567890}
Now you can write your own validation-aware classes and data types.
Lists, similar containers
If Schema(...) encounters an instance of list, tuple, set
or frozenset, it will validate contents of corresponding data container
against all schemas listed inside that container and aggregate all errors:
.. code:: python
>>> Schema([1, 0]).validate([1, 1, 0, 1])
[1, 1, 0, 1]
>>> Schema((int, float)).validate((5, 7, 8, 'not int or float here'))
Traceback (most recent call last):
...
schema.SchemaError: Or(<class 'int'>, <class 'float'>) did not validate 'not int or float here'
'not int or float here' should be instance of 'int'
'not int or float here' should be instance of 'float'
Dictionaries
If ``Schema(...)`` encounters an instance of ``dict``, it will validate data
key-value pairs:
.. code:: python
>>> d = Schema(
... {"name": str, "age": lambda n: 18 <= n <= 99}
... ).validate(
... {"name": "Sue", "age": 28}
... )
>>> assert d == {'name': 'Sue', 'age': 28}
You can specify keys as schemas too:
.. code:: python
>>> schema = Schema({
... str: int, # string keys should have integer values
... int: None, # int keys should be always None
... })
>>> data = schema.validate({
... "key1": 1,
... "key2": 2,
... 10: None,
... 20: None,
... })
>>> schema.validate({
... "key1": 1,
... 10: "not None here",
... })
Traceback (most recent call last):
...
schema.SchemaError: Key '10' error:
None does not match 'not None here'
This is useful if you want to check certain key-values, but don't care
about others:
.. code:: python
>>> schema = Schema({
... "<id>": int,
... "<file>": Use(open),
... str: object, # don't care about other str keys
... })
>>> data = schema.validate({
... "<id>": 10,
... "<file>": "README.rst",
... "--verbose": True,
... })
You can mark a key as optional as follows:
.. code:: python
>>> Schema({
... "name": str,
... Optional("occupation"): str,
... }).validate({"name": "Sam"})
{'name': 'Sam'}
``Optional`` keys can also carry a ``default``, to be used when no key in the
data matches:
.. code:: python
>>> Schema({
... Optional("color", default="blue"): str,
... str: str,
... }).validate({"texture": "furry"}) == {
... "color": "blue",
... "texture": "furry",
... }
True
Defaults are used verbatim, not passed through any validators specified in the
value.
default can also be a callable:
.. code:: python
>>> from schema import Schema, Optional
>>> Schema({Optional('data', default=dict): {}}).validate({}) == {'data': {}}
True
Also, a caveat: If you specify types, **schema** won't validate the empty dict:
.. code:: python
>>> Schema({int:int}).is_valid({})
False
To do that, you need ``Schema(Or({int:int}, {}))``. This is unlike what happens with
lists, where ``Schema([int]).is_valid([])`` will return True.
**schema** has classes ``And`` and ``Or`` that help validating several schemas
for the same data:
.. code:: python
>>> from schema import And, Or
>>> Schema({'age': And(int, lambda n: 0 < n < 99)}).validate({'age': 7})
{'age': 7}
>>> Schema({'password': And(str, lambda s: len(s) > 6)}).validate({'password': 'hai'})
Traceback (most recent call last):
...
schema.SchemaError: Key 'password' error:
<lambda>('hai') should evaluate to True
>>> Schema(And(Or(int, float), lambda x: x > 0)).validate(3.1415)
3.1415
In a dictionary, you can also combine two keys in a "one or the other" manner. To do
so, use the `Or` class as a key:
.. code:: python
>>> from schema import Or, Schema
>>> schema = Schema({
... Or("key1", "key2", only_one=True): str
... })
>>> schema.validate({"key1": "test"}) # Ok
{'key1': 'test'}
>>> schema.validate({"key1": "test", "key2": "test"}) # SchemaError
Traceback (most rece
