Package: schema
schemais 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.ExampleHere is a quick example to get a feeling ofschema, validating a list of
entries with personal information:>>>fromschemaimportSchema,And,Use,Optional,SchemaError>>>schema=Schema([{'name':And(str,len),...'age':And(Use(int),lambdan:18<=n<=99),...Optional('gender'):And(str,Use(str.lower),...lambdas:sin('squid','kid'))}])>>>data=[{'name':'Sue','age':'28','gender':'Squid'},...{'name':'Sam','age':'42'},...{'name':'Sacha','age':'20','gender':'KID'}]>>>validated=schema.validate(data)>>>assertvalidated==[{'name':'Sue','age':28,'gender':'squid'},...{'name':'Sam','age':42},...{'name':'Sacha','age':20,'gender':'kid'}]If data is valid,Schema.validatewill return the validated data
(optionally converted withUsecalls, see below).If data is invalid,Schemawill raiseSchemaErrorexception.
If you just want to check that the data is valid,schema.is_valid(data)will
returnTrueorFalse.InstallationUsepipor easy_install:pip install schemaAlternatively, you can just dropschema.pyfile into your project—it is
self-contained.schemais 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.schemafollowssemantic versioning.HowSchemavalidates dataTypesIfSchema(...)encounters a type (such asint,str,object,
etc.), it will check if the corresponding piece of data is an instance of that type,
otherwise it will raiseSchemaError.>>>fromschemaimportSchema>>>Schema(int).validate(123)123>>>Schema(int).validate('123')Traceback(mostrecentcalllast):...schema.SchemaUnexpectedTypeError:'123'shouldbeinstanceof'int'>>>Schema(object).validate('hai')'hai'CallablesIfSchema(...)encounters a callable (function, class, or object with__call__method) it will call it, and if its return value evaluates toTrueit will continue validating, else—it will raiseSchemaError.>>>importos>>>Schema(os.path.exists).validate('./')'./'>>>Schema(os.path.exists).validate('./non-existent/')Traceback(mostrecentcalllast):...schema.SchemaError:exists('./non-existent/')shouldevaluatetoTrue>>>Schema(lambdan:n>0).validate(123)123>>>Schema(lambdan:n>0).validate(-12)Traceback(mostrecentcalllast):...schema.SchemaError:<lambda>(-12)shouldevaluatetoTrue“Validatables”IfSchema(...)encounters an object with methodvalidateit will run
this method on corresponding data asdata = obj.validate(data). This method
may raiseSchemaErrorexception, which will tellSchemathat that piece
of data is invalid, otherwise—it will continue validating.An example of “validatable” isRegex, that tries to match a string or a
buffer with the given regular expression (itself as a string, buffer or
compiled regexSRE_Pattern):>>>fromschemaimportRegex>>>importre>>>Regex(r'^foo').validate('foobar')'foobar'>>>Regex(r'^[A-Z]+$',flags=re.I).validate('those-dashes-dont-match')Traceback(mostrecentcalllast):...schema.SchemaError:Regex('^[A-Z]+$',flags=re.IGNORECASE)doesnotmatch'those-dashes-dont-match'For a more general case, you can useUsefor creating such objects.Usehelps to use a function or type to convert a value while validating it:>>>fromschemaimportUse>>>Schema(Use(int)).validate('123')123>>>Schema(Use(lambdaf:open(f,'a'))).validate('LICENSE-MIT')<_io.TextIOWrappername='LICENSE-MIT'mode='a'encoding='UTF-8'>Dropping the details,Useis basically:classUse(object):def__init__(self,callable_):self._callable=callable_defvalidate(self,data):try:returnself._callable(data)exceptExceptionase:raiseSchemaError('%rraised%r'%(self._callable.__name__,e))Sometimes you need to transform and validate part of data, but keep original data unchanged.Consthelps to keep your data safe:>>fromschemaimportUse,Const,And,Schema>>fromdatetimeimportdatetime>>is_future=lambdadate:datetime.now()>date>>to_json=lambdav:{"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 containersIfSchema(...)encounters an instance oflist,tuple,setorfrozenset, 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(mostrecentcalllast):...schema.SchemaError:Or(<class'int'>, <class 'float'>) did not validate 'notintorfloathere''not int or float here'shouldbeinstanceof'int''not int or float here'shouldbeinstanceof'float'DictionariesIfSchema(...)encounters an instance ofdict, it will validate data
key-value pairs:>>>d=Schema({'name':str,...'age':lambdan:18<=n<=99}).validate({'name':'Sue','age':28})>>>assertd=={'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(mostrecentcalllast):...schema.SchemaError:Key'10'error:Nonedoesnotmatch'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:>>>fromschemaimportOptional>>>Schema({'name':str,...Optional('occupation'):str}).validate({'name':'Sam'}){'name':'Sam'}Optionalkeys can also carry adefault, to be used when no key in the
data matches:>>>fromschemaimportOptional>>>Schema({Optional('color',default='blue'):str,...str:str}).validate({'texture':'furry'}...)=={'color':'blue','texture':'furry'}TrueDefaults are used verbatim, not passed through any validators specified in the
value.default can also be a callable:>>>fromschemaimportSchema,Optional>>>Schema({Optional('data',default=dict):{}}).validate({})=={'data':{}}TrueAlso, a caveat: If you specify types,schemawon’t validate the empty dict:>>>Schema({int:int}).is_valid({})FalseTo do that, you needSchema(Or({int:int},{})). This is unlike what happens with
lists, whereSchema([int]).is_valid([])will return True.schemahas classesAndandOrthat help validating several schemas
for the same data:>>>fromschemaimportAnd,Or>>>Schema({'age':And(int,lambdan:0<n<99)}).validate({'age':7}){'age':7}>>>Schema({'password':And(str,lambdas:len(s)>6)}).validate({'password':'hai'})Traceback(mostrecentcalllast):...schema.SchemaError:Key'password'error:<lambda>('hai')shouldevaluatetoTrue>>>Schema(And(Or(int,float),lambdax:x>0)).validate(3.1415)3.1415In a dictionary, you can also combine two keys in a “one or the other” manner. To do
so, use theOrclass as a key:>>>fromschemaimportOr,Schema>>>schema=Schema({...Or("key1","key2",only_one=True):str...})>>>schema.validate({"key1":"test"})# Ok{'key1':'test'}>>>schema.validate({"key1":"test","key2":"test"})# SchemaErrorTraceback(mostrecentcalllast):...schema.SchemaOnlyOneAllowedError:TherearemultiplekeyspresentfromtheOr('key1','key2')conditionHooksYou can define hooks which are functions that are executed whenever a valid key:value is found.
TheForbiddenclass is an example of this.You can mark a key as forbidden as follows:>>>fromschemaimportForbidden>>>Schema({Forbidden('age'):object}).validate({'age':50})Traceback(mostrecentcalllast):...schema.SchemaForbiddenKeyError:Forbiddenkeyencountered:'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(mostrecentcalllast):...schema.SchemaForbiddenKeyError:Forbiddenkeyencountered:'age'in{'age':50}You can also define your own hooks. The following hook will call_my_functionifkeyis encountered.fromschemaimportHookdef_my_function(key,scope,error):print(key,scope,error)Hook("key",handler=_my_function)Here’s an example where aDeprecatedclass is added to log warnings whenever a key is encountered:fromschemaimportHook,SchemaclassDeprecated(Hook):def__init__(self,*args,**kwargs):kwargs["handler"]=lambdakey,*args:logging.warn(f"`{key}` is deprecated. "+(self._erroror""))super(Deprecated,self).__init__(*args,**kwargs)Schema({Deprecated("test","custom error message."):object},ignore_extra_keys=True).validate({"test":"value"})...WARNING:`test`isdeprecated.customerrormessage.Extra KeysTheSchema(...)parameterignore_extra_keyscauses 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, useobject: objectas one of the key/value pairs, which will match any key and any value.
Otherwise, extra keys will raise aSchemaError.Customized ValidationTheSchema.validatemethod accepts additional keyword arguments. The
keyword arguments will be propagated to thevalidatemethod of any
child validatables (including any ad-hocSchemaobjects), or the default
value callable (if a callable is specified) forOptionalkeys.This feature can be used together with inheritance of theSchemaclass
for customized validation.Here is an example where a “post-validation” hook that runs after validation
against a sub-schema in a larger schema:classEventSchema(schema.Schema):defvalidate(self,data,_is_event_schema=True):data=super(EventSchema,self).validate(data,_is_event_schema=False)if_is_event_schemaanddata.get("minimum",None)isNone:data["minimum"]=data["capacity"]returndataevents_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)assertevents['event1']['minimum']==1# == capacityassertevents['event2']['minimum']==3Note that the additional keyword argument_is_event_schemais necessary to
limit the customized behavior to theEventSchemaobject itself so that it
won’t affect any recursive invoke of theself.__class__.validatefor the
child schemas (e.g., the call toSchema("capacity").validate("capacity")).User-friendly error reportingYou can pass a keyword argumenterrorto any of validatable classes
(such asSchema,And,Or,Regex,Use) to report this error
instead of a built-in one.>>>Schema(Use(int,error='Invalid year')).validate('XVII')Traceback(mostrecentcalllast):...schema.SchemaError:InvalidyearYou can see all errors that occurred by accessing exception’sexc.autosfor auto-generated error messages, andexc.errorsfor errors
which haderrortext passed to them.You can exit withsys.exit(exc.code)if you want to show the messages
to the user without traceback.errormessages are given precedence in that
case.A JSON API exampleHere is a quick example: validation ofcreate a gistrequest 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"}}}'''>>>fromschemaimportSchema,And,Use,Optional>>>importjson>>>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}UsingschemawithdocoptAssume you are usingdocoptwith 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--countis either integer from 0 to 5, orNone.Assumingdocoptreturns the following dict:>>>args={'<files>':['LICENSE-MIT','setup.py'],...'<path>':'../',...'--count':'3'}this is how you validate it usingschema:>>>fromschemaimportSchema,And,Or,Use>>>importos>>>s=Schema({'<files>':[Use(open)],...'<path>':os.path.exists,...'--count':Or(None,And(Use(int),lambdan:0<n<5))})>>>args=s.validate(args)>>>args['<files>'][<_io.TextIOWrappername='LICENSE-MIT'...>,<_io.TextIOWrappername='setup.py'...]>>>args['<path>']'../'>>>args['--count']3As you can see,schemavalidated data successfully, opened files and
converted'3'toint.JSON schemaYou can also generate standarddraft-07 JSON schemafrom a dictSchema.
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: GeneratingJust define your schema normally and call.json_schema()on it. The output is a Python dict, you need to dump it to JSON.>>>fromschemaimportOptional,Schema>>>importjson>>>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 theLiteralobject instead of a string. The main schema can also have a description.These will appear in IDEs to help your users write a configuration.>>>fromschemaimportLiteral,Schema>>>importjson>>>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 validationsThe 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 theUseandHookobjects for example.ImplementedObject propertiesUse 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 useOptional, 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 Trueat least one key isstrorobjectFor example:Schema({str:str})andSchema({},ignore_extra_keys=True)both becomes{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': True}andSchema({})becomes{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': False}TypesUse the Python type name directly. It will be converted to the JSON name:str->stringint->integerfloat->numberbool->booleanlist->arraydict->objectExample:Schema(float)becomes{"type": "number"}Array itemsSurround a schema with[].Example:Schema([str])means an array of string and becomes:{'type': 'array', 'items': {'type':'string'}}Enumerated valuesUseOr.Example:Schema(Or(1, 2, 3))becomes{"enum": [1, 2, 3]}Constant valuesUse the value itself.Example:Schema("name")becomes{"const": "name"}Regular expressionsUseRegex.Example:Schema(Regex("^v\d+"))becomes{'type': 'string', 'pattern':'^v\\d+'}Annotations (title and description)You can use thenameanddescriptionparameters of theSchemaobject init method.To add description to keys, replace a str with aLiteralobject.Example:Schema({Literal("test",description="Adescription"):str})is equivalent toSchema({"test":str})with the description added to the resulting JSON schema.Combining schemas with allOfUseAndExample:Schema(And(str,"value"))becomes{"allOf":[{"type":"string"},{"const":"value"}]}Note that this example is not really useful in the real world, sinceconstalready implies the type.Combining schemas with anyOfUseOrExample:Schema(Or(str, int))becomes{"anyOf":[{"type":"string"},{"type":"integer"}]}Not implementedThe following JSON schema validations cannot be generated from this library.String lengthHowever, those can be implemented usingRegexString formatHowever, those can be implemented usingRegexObject dependenciesArray lengthArray uniquenessNumeric multiplesNumeric rangesProperty NamesNot implemented. We suggest listing the possible keys instead. As a tip, you can useOras a dict key.Example:Schema({Or("name1","name2"):str})Annotations (default and examples)Combining schemas with oneOfNotObject sizeadditionalProperties having a different schema (true and false is supported)JSON: Minimizing output sizeExplicit ReuseIf 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>>>fromschemaimportOptional,Schema>>>importjson>>>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>>>fromschemaimportOptional,Or,Schema>>>importjson>>>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 reuseIf 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 parameteruse_refsto 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.>>>fromschemaimportOptional,Or,Schema>>>importjson>>>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#"}