There are a lot of times when you need to validate incoming input either from a form or from an incoming json request. It is wise to have some form of backend validation as it will allow you to build more secure applications. Masonite provides an extremely flexible and fluent way to validate this data.
Validations are based on rules where you can pass in a key or a list of keys to the rule. The validation will then use all the rules and apply them to the dictionary to validate.
Incoming form or JSON data can be validated very simply. All you need to do is import the Validator class, resolve it, and use the necessary rule methods.
This whole snippet will look like this in your controller method:
If you want to handle errors in views specifically you will need to add the ShareErrorsInSessionMiddleware middleware into your route middlewares. errors will be injected to views as a MessageBag instance allowing to handle errors easily:
Sometimes you may cross a time where you need to create a new rule that isn't available in Masonite or there is such a niche use case that you need to build a rule for.
In this case you can create a new rule.
Rule Command
You can easily create a new rule boiler plate by running:
$pythoncraftruleequals_masonite
There is no particular reason that rules are lowercase class names. The main reason is that it improves readability when you end up using it as a method if you choose to register the rule with the validation class like you will see below.
This will create a boiler plate rule inside app/rules/equals_masonite.py that looks like:
classequals_masonite(BaseValidation):"""A rule_name validation class """defpasses(self,attribute,key,dictionary):"""The passing criteria for this rule. ... """return attributedefmessage(self,key):"""A message to show when this rule fails ... """return'{} is required'.format(key)defnegated_message(self,key):"""A message to show when this rule is negated using a negation rule like 'isnt()' ... """return'{} is not required'.format(key)
Constructing our Rule
Our rule class needs 3 methods that you see when you run the rule command, a passes, message and negated_message methods.
Passes Method
The passes method needs to return some kind of boolean value for the use case in which this rule passes.
For example if you need to make a rule that a value always equals Masonite then you can make the method look like this:
defpasses(self,attribute,key,dictionary):"""The passing criteria for this rule. ... """return attribute =='Masonite'
When validating a dictionary like this:
{'name':'Masonite'}
then
the attribute will be the value (Masonite)
the key will be the dictionary key (name)
the dictionary will be the full dictionary in case you need to do any additional checks.
Message method
The message method needs to return a string used as the error message. If you are making the rule above then our rule may so far look something like:
defpasses(self,attribute,key,dictionary):"""The passing criteria for this rule. ... """return attribute =='Masonite'defmessage(self,key):return'{} must be equal to Masonite'.format(key)
Negated Message
The negated message method needs to return a message when this rule is negated. This will basically be a negated statement of the message method:
defpasses(self,attribute,key,dictionary):"""The passing criteria for this rule. ... """return attribute =='Masonite'defmessage(self,key):return'{} must be equal to Masonite'.format(key)defnegated_message(self,key):return'{} must not be equal to Masonite'.format(key)
Registering our Rule
Now the rule is created we can use it in 1 of 2 ways.
Importing our rule
We can either import directly into our controller method:
or we can register our rule and use it with the Validator class as normal.
Register the rule
In any service provider's boot method (preferably a provider where wsgi=False to prevent it from running on every request) we can register our rule with the validator class.
If you don't have a provider yet we can make one specifically for adding custom rules:
$pythoncraftproviderRuleProvider
Then inside this rule provider's boot method we can resolve and register our rule. This will look like:
from app.rules.equals_masonite import equals_masonitefrom masonite.validation import ValidatorclassRuleProvider(ServiceProvider):"""Provides Services To The Service Container """def__init__(self,application): self.application = applicationdefregister(self,validator: Validator):"""Boots services required by the container """ self.application.make('validator').register(equals_masonite)
Now instead of importing the rule we can just use it as normal:
Just put the dictionary as the first argument and then each rule being its own argument.
Rule Enclosures
Rule enclosures are self contained classes with rules. You can use these to help reuse your validation logic. For example if you see you are using the same rules often you can use an enclosure to always keep them together and reuse them throughout your code base.
Rule Enclosure Command
You can create a rule enclosure by running:
$ python craft rule:enclosure AcceptedTerms
You will then see a file generated like this inside app/rules:
from masonite.validation import RuleEnclosure...classAcceptedTerms(RuleEnclosure):defrules(self):""" ... """return [# Rules go here ]
Creating the Rule Enclosure
You can then fill the list with rules:
from masonite.validation import required, acceptedclassLoginForm(RuleEnclosure):defrules(self):""" ... """return [required(['email', 'terms']),accepted('terms') ]
You can easily get all errors using the all() method:
errors.all()"""{ 'email': ['Your email is required'], 'name': ['Your name is required']}"""
Checking for any errors
errors.any()#== True
Checking if the bag is Empty
This is just the opposite of the any() method.
errors.empty()#== False
Checking For a Specific Error
errors.has('email')#== True
Getting the first Key:
errors.all()"""{ 'email': ['Your email is required'], 'name': ['Your name is required']}"""errors.first()"""{ 'email': ['Your email is required']}"""
Getting the Number of Errors:
errors.count()#== 2
Converting to JSON
errors.json()"""'{"email": ["Your email is required"],"name": ["Your name is required"]}'"""
Get the Amount of Messages:
errors.amount('email')#== 1
Get the Messages:
errors.get('email')"""['Your email is required']"""
Get the Errors
errors.errors()"""['email', 'name']"""
Get all the Messages:
errors.messages()"""['Your email is required', 'Your name is required']"""
Merge a Dictionary
You can also merge an existing dictionary into the bag with the errors:
errors.merge({'key': 'value'})
Nested Validations
Sometimes you will need to check values that aren't on the top level of a dictionary like the examples shown here. In this case we can use dot notation to validate deeper dictionaries:
notice the dot notation here. Each . being a deeper level to the dictionary.
Nested Validations With Lists
Sometimes your validations will have lists and you will need to ensure that each element in the list validates. For example you want to make sure that a user passes in a list of names and ID's.
For this you can use the * asterisk to validate these:
All errors returned will be very generic. Most times you will need to specify some custom error that is more tailored to your user base.
Each rule has a messages keyword arg that can be used to specify your custom errors.
"""{ 'terms': 'off', 'active': 'on',}"""validate.accepted(['terms', 'active'], messages = {'terms': 'You must check the terms box on the bottom','active': 'Make sure you are active'})
Now instead of returning the generic errors, the error message returned will be the one you supplied.
Leaving out a message will result in the generic one still being returned for that value.
Exceptions
By default, Masonite will not throw exceptions when it encounters failed validations. You can force Masonite to raise a ValueError when it hits a failed validation:
You can also specify which exceptions should be thrown with which key being checked by using a dictionary:
try: errors = request.validate( validate.required(['user.email', 'user.id'], raises={'user.id': AttributeError,'user.email': CustomException }), )exceptAttributeErroras e:str(e)#== 'user.id is required'except CustomException as e:str(e)#== 'user.email is required'
All other rules within an explicit exception error will throw the ValueError.
String Validation
In addition to using the methods provided below, you can also use each one as a pipe delimitted string. For example these two validations are identical:
These rules are identical so use whichever feels more comfortable.
Available Rules
Accepted
The accepted rule is most useful when seeing if a checkbox has been checked. When a checkbox is submitted it usually has the value of on so this rule will check to make sure the value is either on, 1, or yes.
"""{ 'terms': 'on'}"""validate.accepted('terms')
Active_domain
This is used to verify that the domain being passed in is a DNS resolvable domain name. You can also do this for email addresses as well. The preferred search is domain.com but Masonite will strip out http://, https:// and www automatically for you.
This is used to make sure a value exists inside an iterable (like a list or string). You may want to check if the string contains the value Masonite for example:
"""{ 'description': 'Masonite is an amazing framework'}"""validate.contains('description', 'Masonite')
Date
This is used to verify that the value is a valid date. Pendulum module is used to verify validity. It supports the RFC 3339 format, most ISO 8601 formats and some other common formats.
Additionally you can check file size, with different file size formats:
validate.file('document', 1024) # check valid file and max size is 1 Kilobyte (1024 bytes)
validate.file('document', '1K') # check valid file and max size is 1 Kilobyte (1024 bytes), 1k or 1KB also works
validate.file('document', '15M') # check valid file and max size is 15 Megabytes
Finally file type can be checked through a MIME types list:
Valid image types are defined by all MIME types starting with image/. For more details you can check mimetypes Python package which gives known MIME types with mimetypes.types_map.
Additionally you can check image size as with basic file validator
validate.image('avatar', size="2MB")
In_range
Used when you need to check if an integer is within a given range of numbers
Every country has their own postal code formats. We added regular expressions for over 130 countries which you can specify by using a comma separated string of country codes:
Please look up the "alpha-2 code" for available country formats.
Regex
Sometimes you want to do more complex validations on some fields. This rule allows to validate against a regular expression directly. In the following example we check that username value is a valid user name (without special characters and between 3 and 16 characters).
Used to make sure the value is actually available in the dictionary and not null. This will add errors if the key is not present. To check only the presence of the value in the dictionary use exists.
"""
{
'age': 25,
'email': 'user@email.com',
'first_name': ''
}
"""
validate.required(['age', 'email'])
validate.required('first_name') # would fail
validate.required('last_name') # would fail
Required If
Used to make sure that value is present and not empty only if an other field has a given value.
The strong rule is used to make sure a string has a certain amount of characters required to be considered a "strong" string.
This is really useful for passwords when you want to make sure a password has at least 8 characters, have at least 2 uppercase letters and at least 2 special characters.
Used to check that a value is a valid UUID. The UUID version (according to RFC 4122) standard can optionally be verified (1,3,4 or 5). The default version 4.
"""
{
'doc_id': 'c1d38bb1-139e-4099-8a20-61a2a0c9b996'
}
"""
# check value is a valid UUID4
validate.uuid('doc_id')
# check value is a valid UUID3
validate.uuid('doc_id', 3) # or '3'
Video
Used to make sure that value is a valid video file.
Valid video types are defined by all MIME types starting with video/. For more details you can check mimetypes Python package which gives known MIME types with mimetypes.types_map.
Additionally you can check video size as with basic file validator
validate.video('document', size="2MB")
When
Conditional rules. This is used when you want to run a specific set of rules only if a first set of rules succeeds.
For example if you want to make terms be accepted ONLY if the user is under 18