Package: val
A validator for arbitrary Python objects. Works with Python 2 and 3.Inspired by some of the wonderful ideas inschemaandflatland, many of which
I outright stole.The goal is to make validation faster than either, while keeping the very
pythonic and minimal style ofschema, at the expense of more advanced
features.Current status is: used in production code, but only in one place that I know
of.I have not optimized much, but for the kind of schemas I need (specifically: to
validate JSON that has been loaded into python structures as part of a REST
API,) I have some anecdotal evidence that it’s around ten times faster than
both schema and flatland. (Again, that is mostly because it does way less.)The schemas understood by val are very similar to the ones inschema, but not
100% compatible.SyntaxElements that can occur in a schema are:LiteralsSimple literal values will match equal values:>>>fromvalimportSchema>>>Schema(12).validates(12)True>>>Schema('foo').validates('foo')TrueTypesTypes and classes will validate anything that is an instance of the type:>>>Schema(int).validates(12)True>>>Schema(str).validates('foo')True>>>Schema(str).validates('fnørd')True>>>Schema(list).validates([12,'foo'])True>>>Schema(dict).validates({'foo':12})True>>>classFoo(object):...pass>>>instance=Foo()>>>Schema(Foo).validates(instance)True>>>classSubClass(Foo):...pass>>>subclass_instance=SubClass()>>>Schema(Foo).validates(subclass_instance)True>>>schema=Schema(object)>>>all(schema.validates(thing)forthingin[...instance,(12,43,'strawberries'),{}])TrueListsLists will validate list values all of whose elements are validated by at least
one of the elements in the schema (order or number of elements do not matter,
seeOrdered()):>>>Schema([str,int]).validates([12,'foo','bar','baz',42])True>>>schema=Schema(['foo','bar',13])>>>schema.validates(['foo'])True>>>schema.validates(['foo',13])True>>>schema.validates(['bar','bar',13,'bar'])TrueDictionariesDictionaries will validate dictionaries all of whose key value pairs are
validated by at least one of the key value pairs in the schema, and that are
not missing any of the keys specified (unless they are specified asOptional()):>>>schema=Schema({'foo':int,str:int})>>>schema.validates({'foo':83})True>>>schema.validates({'foo':12,'bar':888,'baz':299})True>>>schema.validate({'foo':'bar'})Traceback(mostrecentcalllast):...val.NotValid:'foo':'bar'isnotoftype<class'int'>>>>schema.validate({'qux':19})Traceback(mostrecentcalllast):...val.NotValid:missingkey:'foo'>>>schema.validate({'foo':21,12:'bar'})Traceback(mostrecentcalllast):...val.NotValid:12:'bar'notmatchedCallablesCallables (that aren’t of typetype) will validate any value for which the
callable returns a truthy value. TypeErrors or ValueErrors in the call will
result in a NotValid exception:>>>schema=Schema(lambdax:x<10)>>>schema.validates(9)True>>>schema.validate(10)Traceback(mostrecentcalllast):...val.NotValid:10invalidatedby'<lambda>'To get nicer error messages, use functions rather than lambdas (if the function
has a doc string it will be used in the error message, otherwise the name of
the funtion will):>>>defless_than_ten(n):..."""Must be less than 10."""...returnn<10>>>schema=Schema(less_than_ten)>>>schema.validates(9)True>>>schema.validate(10)Traceback(mostrecentcalllast):...val.NotValid:10invalidatedby'Must be less than 10.'Convert()Convert(callable)will call the callable on the value being validated,
and substitute the result of that call for the original value in the
validated structure. TypeErrors or ValueErrors in the call will result in a
NotValid exception. This or supplyingDefault Valuesare the only ways to
modify the data during validation. For that reason it should be used sparingly.Convert is useful to convert between representations (for instance from
timestamps to datetime objects, or uuid string representations to uuid objects,
etc.):>>>fromvalimportConvert>>>schema=Schema(Convert(int))>>>schema.validate('12')12>>>schema.validate(42.34)42>>>schema.validate('foo')Traceback(mostrecentcalllast):...val.NotValid:invalidliteralforint()withbase10:'foo'Or()Or(element1, element2,...)will validate a value validated by any of the
elements passed into the Or:>>>fromvalimportOr>>>schema=Or('foo',int)>>>schema.validates('foo')True>>>schema.validates(12)True>>>schema.validate('bar')Traceback(mostrecentcalllast):...val.NotValid:'bar'isnotequalto'foo','bar'isnotoftype<class'int'>And()And(element1, element2,...)will validate a value validated by all of
the elements passed into the And:>>>fromvalimportAnd>>>schema=And(Convert(int),lambdax:x<12,lambdax:x>=3)>>>schema.validate('3')3>>>schema.validate(11.6)11>>>schema.validate('12')Traceback(mostrecentcalllast):...val.NotValid:12invalidatedby'<lambda>'>>>schema.validate(42.77)Traceback(mostrecentcalllast):...val.NotValid:42invalidatedby'<lambda>'>>>schema.validate('foo')Traceback(mostrecentcalllast):...val.NotValid:invalidliteralforint()withbase10:'foo'Optional(){Optional(simple_literal_key): value}will match any key value pair that
matchessimple_literal_key: valuebut the schema will still validate
dictionary values with no matching key.>>>fromvalimportOptional>>>schema=Schema({Optional('foo'):12})>>>schema.validates({'foo':12})True>>>schema.validates({})True>>>schema.validate({'foo':13})Traceback(mostrecentcalllast):...val.NotValid:'foo':13isnotequalto12>>>schema.validate({'foo':'bar'})Traceback(mostrecentcalllast):...val.NotValid:'foo':'bar'isnotequalto12Ordered()Ordered([element1,element2, element3])will validate a list withexactly3 elements, each of which must be validated by the corresponding
element in the schema. If order and number of elements do not matter, just
useLists:>>>fromvalimportOrdered>>>schema=Ordered([int,str,int,None])>>>schema.validates([12,'fnord',42,None])True>>>schema.validate(['fnord',42,None,12])Traceback(mostrecentcalllast):...val.NotValid:'fnord'isnotoftype<class'int'>>>>schema.validate([12,'fnord',42,None,12])Traceback(mostrecentcalllast):...val.NotValid:[12,'fnord',42,None,12]doesnothaveexactly4values.(Got5.)Parsed SchemasOther parsed schema objects. So this works:>>>sub_schema=Schema({'foo':str,str:int})>>>schema=Schema(...{'key1':sub_schema,...'key2':sub_schema,...str:sub_schema})>>>schema.validates({...'key1':{'foo':'bar'},...'key2':{'foo':'qux','baz':43},...'whatever':{'foo':'doo','fsck':22,'tsk':2992}})TrueFAQHow do I validate only some of the keys in a dictionary?Often when validating input there will be values present that your code doesn’t
act upon, and doesn’t care about the presence or absence of. You can make your
schema similarly indifferent by addingstr: object(assuming the keys in
the dictionary are all strings, like they are when your data comes from JSON.
If even the type of the keys is variable, you can useobject: object.) This
will match and validate any keys in the dictionary that you didn’t explicitly
specify.>>>schema=Schema({...'username':str,...'password':str,...str:object})>>>schema.validates({...'username':'bob',...'password':'hella rancid hazelnuts',...'shopping_cart':{...'contents':['Meet the Parens: A Lisp primer.']}})True>>>schema.validate({...'username':'connie',...'goldfish':12})Traceback(mostrecentcalllast):...val.NotValid:missingkey:'password'Advanced TopicsDefault ValuesOne can supply a default value to any (subclass of) Schema, which will be used
in place of the validated value if that evaluates toFalse.>>>schema=Schema(str,default='default value')>>>schema.validate('supplied value')'supplied value'>>>schema.validate('')'default value'Note that the original value must still be valid for the schema, so this will
not work:>>>schema.validates(None)FalseBut this will:>>>schema=Or(str,None,default='default value')>>>schema.validate(None)'default value'Default values will also work for dictionary keys that are specified asOptional:>>>schema=Schema(...{'foo':str,...Optional('bar'):Or(int,None,default=23)})>>>schema.validate({'foo':'yes'})=={'bar':23,'foo':'yes'}TrueAdditional ValidatorsSometimes it is useful to do validation that depends on multiple parts of the
data at once. For this purpose, Schemas can be initialized with additional
validators.>>>defmaximum_total(value):..."""The total sum must not exceed 500."""...returnsum(value.values())<=500>>>schema=Schema({str:int},additional_validators=(maximum_total,))>>>schema.validates({'foo':12,'bar':400})True>>>schema.validate({'foo':250,'bar':251})Traceback(mostrecentcalllast):...val.NotValid:...invalidatedby'The total sum must not exceed 500.'Serializing SchemasWhen your application receives JSON from clients, it can be useful to define
explicit schemas that those clients have to abide by. Pointing to source code
isn’t an especially great way to communicate to other developers what is or
isn’t considered valid JSON by your application, especially if they aren’t
developing in Python. For this purpose,teleport, a lightweight JSON format to
describe schemas, is better suited.A subset of valid val schemas is serializable/exportable toteleport.
Note that things like default values and additional validators will be lost
when serializing to teleport, because it has no way to express them.Combining doctests with this serialization provides a way to specify what your
application considers valid, and verify in your tests that you didn’t
unintentionally break clients’ assumptions.If your code contains the following schema for todo items:>>>todo=Schema({..."task":str,...Optional("priority"):int,...Optional("status"):str})Then in your API documentation you could use thedocument()helper and
have doctests verify the output, as is the case here.>>>fromvalimporttp>>>print(tp.document(todo)){"Struct":{"optional":{"priority":"Integer","status":"String"},"required":{"task":"String"}}}