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.
Example
Here is a quick example to get a feeling of schema, validating a list of entries with personal information:
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 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.
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
.
>>> 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
.
>>> 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
):
>>> 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:
>>> 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:
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:
>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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 recent call last):
...
schema.SchemaOnlyOneAllowedError: There are multiple keys present from the Or('key1', 'key2') condition
Hooks
You can define hooks which are functions that are executed whenever a valid key:value is found. The Forbidden class is an example of this.
You can mark a key as forbidden as follows:
>>> from schema import Forbidden
>>> Schema({Forbidden('age'): object}).validate({'age': 50})
Traceback (most recent call last):
...
schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50}
A few things are worth noting. First, the value paired with the forbidden key determines whether it will be rejected:
>>> Schema({Forbidden('age'): str, 'age': int}).validate({'age': 50})
{'age': 50}
Note: if we hadn't supplied the 'age' key here, the call would have failed too, but with SchemaWrongKeyError, not SchemaForbiddenKeyError.
Second, Forbidden has a higher priority than standard keys, and consequently than Optional. This means we can do that:
>>> Schema({Forbidden('age'): object, Optional(str): object}).validate({'age': 50})
Traceback (most recent call last):
...
schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50}
You can also define your own hooks. The following hook will call _my_function if key is encountered.
from schema import Hook
def _my_function(key, scope, error):
print(key, scope, error)
Hook("key", handler=_my_function)
Here's an example where a Deprecated class is added to log warnings whenever a key is encountered:
from schema import Hook, Schema
class Deprecated(Hook):
def __init__(self, *args, **kwargs):
kwargs["handler"] = lambda key, *args: logging.warn(f"`{key}` is deprecated. " + (self._error or ""))
super(Deprecated, self).__init__(*args, **kwargs)
Schema({Deprecated("test", "custom error message."): object}, ignore_extra_keys=True).validate({"test": "value"})
...
WARNING: `test` is deprecated. custom error message.
Extra Keys
The Schema(...)
parameter ignore_extra_keys
causes validation to
ignore extra keys in a dictionary, and also to not return them after
validating.
>>> schema = Schema({'name': str}, ignore_extra_keys=True)
>>> schema.validate({'name': 'Sam', 'age': '42'})
{'name': 'Sam'}
If you would like any extra keys returned, use object: object
as one
of the key/value pairs, which will match any key and any value.
Otherwise, extra keys will raise a SchemaError
.
Customized Validation
The Schema.validate
method accepts additional keyword arguments. The
keyword arguments will be propagated to the validate
method of any
child validatables (including any ad-hoc Schema
objects), or the
default value callable (if a callable is specified) for Optional
keys.
This feature can be used together with inheritance of the Schema
class
for customized validation.
Here is an example where a "post-validation" hook that runs after validation against a sub-schema in a larger schema:
class EventSchema(schema.Schema):
def validate(self, data, _is_event_schema=True):
data = super(EventSchema, self).validate(data, _is_event_schema=False)
if _is_event_schema and data.get("minimum", None) is None:
data["minimum"] = data["capacity"]
return data
events_schema = schema.Schema(
{
str: EventSchema({
"capacity": int,
schema.Optional("minimum"): int, # default to capacity
})
}
)
data = {'event1': {'capacity': 1}, 'event2': {'capacity': 2, 'minimum': 3}}
events = events_schema.validate(data)
assert events['event1']['minimum'] == 1 # == capacity
assert events['event2']['minimum'] == 3
Note that the additional keyword argument _is_event_schema
is
necessary to limit the customized behavior to the EventSchema
object
itself so that it won't affect any recursive invoke of the
self.__class__.validate
for the child schemas (e.g., the call to
Schema("capacity").validate("capacity")
).
User-friendly error reporting
You can pass a keyword argument error
to any of validatable classes
(such as Schema
, And
, Or
, Regex
, Use
) to report this error
instead of a built-in one.
>>> Schema(Use(int, error='Invalid year')).validate('XVII')
Traceback (most recent call last):
...
schema.SchemaError: Invalid year
You can see all errors that occurred by accessing exception's
exc.autos
for auto-generated error messages, and exc.errors
for
errors which had error
text passed to them.
You can exit with sys.exit(exc.code)
if you want to show the messages
to the user without traceback. error
messages are given precedence in
that case.
A JSON API example
Here is a quick example: validation of create a gist request from github API.
>>> gist = '''{"description": "the description for this gist",
... "public": true,
... "files": {
... "file1.txt": {"content": "String file contents"},
... "other.txt": {"content": "Another file contents"}}}'''
>>> from schema import Schema, And, Use, Optional
>>> import json
>>> gist_schema = Schema(
... And(
... Use(json.loads), # first convert from JSON
... # use str since json returns unicode
... {
... Optional("description"): str,
... "public": bool,
... "files": {str: {"content": str}},
... },
... )
... )
>>> gist = gist_schema.validate(gist)
# gist:
{u'description': u'the description for this gist',
u'files': {u'file1.txt': {u'content': u'String file contents'},
u'other.txt': {u'content': u'Another file contents'}},
u'public': True}
Using schema with docopt
Assume you are using docopt with the following usage-pattern:
Usage: my_program.py [--count=N] <path> <files>...
and you would like to validate that <files>
are readable, and that
<path>
exists, and that --count
is either integer from 0 to 5, or
None
.
Assuming docopt returns the following dict:
>>> args = {
... "<files>": ["LICENSE-MIT", "setup.py"],
... "<path>": "../",
... "--count": "3",
... }
this is how you validate it using schema
:
>>> from schema import Schema, And, Or, Use
>>> import os
>>> s = Schema({
... "<files>": [Use(open)],
... "<path>": os.path.exists,
... "--count": Or(None, And(Use(int), lambda n: 0 < n < 5)),
... })
>>> args = s.validate(args)
>>> args['<files>']
[<_io.TextIOWrapper name='LICENSE-MIT' ...>, <_io.TextIOWrapper name='setup.py' ...]
>>> args['<path>']
'../'
>>> args['--count']
3
As you can see, schema validated data successfully, opened files and
converted '3'
to int
.
JSON schema
You can also generate standard draft-07 JSON
schema from a dict Schema
. This can be used
to add word completion, validation, and documentation directly in code
editors. The output schema can also be used with JSON schema compatible
libraries.
JSON: Generating
Just define your schema normally and call .json_schema()
on it. The
output is a Python dict, you need to dump it to JSON.
>>> from schema import Optional, Schema
>>> import json
>>> s = Schema({
... "test": str,
... "nested": {Optional("other"): str},
... })
>>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"))
# json_schema
{
"type":"object",
"properties": {
"test": {"type": "string"},
"nested": {
"type":"object",
"properties": {
"other": {"type": "string"}
},
"required": [],
"additionalProperties": false
}
},
"required":[
"test",
"nested"
],
"additionalProperties":false,
"$id":"https://example.com/my-schema.json",
"$schema":"http://json-schema.org/draft-07/schema#"
}
You can add descriptions for the schema elements using the Literal
object instead of a string. The main schema can also have a description.
These will appear in IDEs to help your users write a configuration.
>>> from schema import Literal, Schema
>>> import json
>>> s = Schema(
... {Literal("project_name", description="Names must be unique"): str},
... description="Project schema",
... )
>>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4)
# json_schema
{
"type": "object",
"properties": {
"project_name": {
"description": "Names must be unique",
"type": "string"
}
},
"required": [
"project_name"
],
"additionalProperties": false,
"$id": "https://example.com/my-schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Project schema"
}
JSON: Supported validations
The resulting JSON schema is not guaranteed to accept the same objects
as the library would accept, since some validations are not implemented
or have no JSON schema equivalent. This is the case of the Use
and
Hook
objects for example.
Implemented
: Use a dict literal. The dict keys are the JSON schema properties.
Example:
`Schema({"test": str})`
becomes
`{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': ['test'], 'additionalProperties': False}`.
Please note that attributes are required by default. To create
optional attributes use `Optional`, like so:
`Schema({Optional("test"): str})`
becomes
`{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': [], 'additionalProperties': False}`
additionalProperties is set to true when at least one of the conditions is met:
: - ignore_extra_keys is True
- at least one key is [str]{.title-ref} or
[object]{.title-ref}
For example:
`Schema({str: str})` and `Schema({}, ignore_extra_keys=True)`
both becomes
`{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': True}`
and
`Schema({})`
becomes
`{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': False}`
Types
: Use the Python type name directly. It will be converted to the JSON name:
- `str` -\>
[string](https://json-schema.org/understanding-json-schema/reference/string.html)
- `int` -\>
[integer](https://json-schema.org/understanding-json-schema/reference/numeric.html#integer)
- `float` -\>
[number](https://json-schema.org/understanding-json-schema/reference/numeric.html#number)
- `bool` -\>
[boolean](https://json-schema.org/understanding-json-schema/reference/boolean.html)
- `list` -\>
[array](https://json-schema.org/understanding-json-schema/reference/array.html)
- `dict` -\>
[object](https://json-schema.org/understanding-json-schema/reference/object.html)
Example:
`Schema(float)`
becomes
`{"type": "number"}`
: Surround a schema with []
.
Example:
`Schema([str])` means an array of string and becomes:
`{'type': 'array', 'items': {'type': 'string'}}`
: Use Or.
Example:
`Schema(Or(1, 2, 3))` becomes
`{"enum": [1, 2, 3]}`
: Use the value itself.
Example:
`Schema("name")` becomes
`{"const": "name"}`
: Use Regex
.
Example:
`Schema(Regex("^v\d+"))` becomes
`{'type': 'string', 'pattern': '^v\\d+'}`
Annotations (title and description)
: You can use the name
and description
parameters of the Schema
object init method.
To add description to keys, replace a str with a `Literal` object.
Example:
`Schema({Literal("test", description="A description"): str})`
is equivalent to
`Schema({"test": str})`
with the description added to the resulting JSON schema.
: Use And
Example:
`Schema(And(str, "value"))`
becomes
`{"allOf": [{"type": "string"}, {"const": "value"}]}`
Note that this example is not really useful in the real world, since
`const` already implies the type.
: Use Or
Example:
`Schema(Or(str, int))`
becomes
`{"anyOf": [{"type": "string"}, {"type": "integer"}]}`
Not implemented
The following JSON schema validations cannot be generated from this library.
[String length](https://json-schema.org/understanding-json-schema/reference/string.html#length)
: However, those can be implemented using `Regex`
[String format](https://json-schema.org/understanding-json-schema/reference/string.html#format)
: However, those can be implemented using `Regex`
[Property Names](https://json-schema.org/understanding-json-schema/reference/object.html#property-names)
: Not implemented. We suggest listing the possible keys instead.
As a tip, you can use `Or` as a dict key.
Example:
`Schema({Or("name1", "name2"): str})`
-
additionalProperties having a different schema (true and false is supported)
JSON: Minimizing output size
Explicit Reuse
If your JSON schema is big and has a lot of repetition, it can be made simpler and smaller by defining Schema objects as reference. These references will be placed in a "definitions" section in the main schema.
You can look at the JSON schema documentation for more information
>>> from schema import Optional, Schema
>>> import json
>>> s = Schema({
... "test": str,
... "nested": Schema({Optional("other"): str}, name="nested", as_reference=True)
... })
>>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4)
# json_schema
{
"type": "object",
"properties": {
"test": {
"type": "string"
},
"nested": {
"$ref": "#/definitions/nested"
}
},
"required": [
"test",
"nested"
],
"additionalProperties": false,
"$id": "https://example.com/my-schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"nested": {
"type": "object",
"properties": {
"other": {
"type": "string"
}
},
"required": [],
"additionalProperties": false
}
}
}
This becomes really useful when using the same object several times
>>> from schema import Optional, Or, Schema
>>> import json
>>> language_configuration = Schema(
... {"autocomplete": bool, "stop_words": [str]},
... name="language",
... as_reference=True,
... )
>>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration})
>>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4)
# json_schema
{
"type": "object",
"properties": {
"ar": {
"$ref": "#/definitions/language"
},
"cs": {
"$ref": "#/definitions/language"
},
"de": {
"$ref": "#/definitions/language"
},
"el": {
"$ref": "#/definitions/language"
},
"eu": {
"$ref": "#/definitions/language"
},
"en": {
"$ref": "#/definitions/language"
},
"es": {
"$ref": "#/definitions/language"
},
"fr": {
"$ref": "#/definitions/language"
}
},
"required": [],
"additionalProperties": false,
"$id": "https://example.com/my-schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"language": {
"type": "object",
"properties": {
"autocomplete": {
"type": "boolean"
},
"stop_words": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"autocomplete",
"stop_words"
],
"additionalProperties": false
}
}
}
Automatic reuse
If you want to minimize the output size without using names explicitly, you can have the library generate hashes of parts of the output JSON schema and use them as references throughout.
Enable this behaviour by providing the parameter use_refs
to the
json_schema method.
Be aware that this method is less often compatible with IDEs and JSON schema libraries. It produces a JSON schema that is more difficult to read by humans.
>>> from schema import Optional, Or, Schema
>>> import json
>>> language_configuration = Schema({"autocomplete": bool, "stop_words": [str]})
>>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration})
>>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json", use_refs=True), indent=4)
# json_schema
{
"type": "object",
"properties": {
"ar": {
"type": "object",
"properties": {
"autocomplete": {
"type": "boolean",
"$id": "#6456104181059880193"
},
"stop_words": {
"type": "array",
"items": {
"type": "string",
"$id": "#1856069563381977338"
}
}
},
"required": [
"autocomplete",
"stop_words"
],
"additionalProperties": false
},
"cs": {
"type": "object",
"properties": {
"autocomplete": {
"$ref": "#6456104181059880193"
},
"stop_words": {
"type": "array",
"items": {
"$ref": "#1856069563381977338"
},
"$id": "#-5377945144312515805"
}
},
"required": [
"autocomplete",
"stop_words"
],
"additionalProperties": false
},
"de": {
"type": "object",
"properties": {
"autocomplete": {
"$ref": "#6456104181059880193"
},
"stop_words": {
"$ref": "#-5377945144312515805"
}
},
"required": [
"autocomplete",
"stop_words"
],
"additionalProperties": false,
"$id": "#-8142886105174600858"
},
"el": {
"$ref": "#-8142886105174600858"
},
"eu": {
"$ref": "#-8142886105174600858"
},
"en": {
"$ref": "#-8142886105174600858"
},
"es": {
"$ref": "#-8142886105174600858"
},
"fr": {
"$ref": "#-8142886105174600858"
}
},
"required": [],
"additionalProperties": false,
"$id": "https://example.com/my-schema.json",
"$schema": "http://json-schema.org/draft-07/schema#"
}