Error Handling#
This page covers the methods of dealing with validation errors and customizing the default error handling system.
Introduction#
It is recommended that you see the Error handling section in the Tutorial section for a basic overview of how errors work in Oblate.
ValidationError and FieldError#
It is important to understand the two primary exception classes that Oblate uses for validation errors:
FieldError and ValidationError.
FieldError is raised when validation fails for a specific field. This error can be raised
from the user side code to indicate validation failures.
ValidationError is raised when validation fails for a schema with one or more FieldError.
This error is not generally raised by the user but by the library. ValidationError can be seen
as a “wrapped” form of FieldError. It includes all the field errors that caused the validation
failure.
In more simpler terms, you should always use ValidationError when you are initializing or
updating a Schema and want to catch any validation errors and you should raise the
FieldError in your validators or other user side code to indicate validation failure.
User side errors#
When you are working with validators, you would be raising FieldError
to indicate validation failures.
FieldError raised in any user side code is accounted as a validation error and it is wrapped
by the subsequently raised ValidationError. For ease and convenience, certain standard errors
are also supported to be raised in user code to indicate validation failure. These are:
ValueErrorAssertionError
This means that if you raise on of the above errors in your validators, it will automatically be
converted to a FieldError. You can also use assert statements which makes the code
more concise.
Example:
@validate.field('id')
def validate_id(self, value, ctx):
if value > 100:
raise oblate.FieldError('Invalid ID, must be less than 100')
# is equivalent to:
@validate.field('id')
def validate_id(self, value, ctx):
assert not value > 100, 'Invalid ID, must be less than 100'
Warning
It is recommended to raise ValueError if you intend to run your script using
the Python’s -O or -OO optimization flags. These flags remove the assertion
statements from the code causing the validators to stop working properly.
Error formatting#
When an error is raised, it is presented in a specific format. When ValidationError is raised
and printed in the console it looks somewhat like this:
oblate.exceptions.ValidationError:
│
│ 2 validation errors in schema 'User'
│
└── In field id:
└── Value of this field must be an integer
│
└── In field username:
└── This field is required.
Indicating each field that caused the error alongside the error message. The messages are indented to make the error more easily readable. This format also nicely adapts to Nested fields:
oblate.exceptions.ValidationError:
│
│ 2 validation errors in schema 'Film'
│
└── In field actor:
│
└── In field name:
└── Value of this field must be a string
│
└── In field rating:
└── This field is required.
In this case, actor was a nested schema which had errors so the level of indentation increases
with level of nesting for indicating errors in nested fields.
Raw error formatting#
ValidationError can also be converted to “raw” form which is essentially a dictionary
for detailing the error. This is done by the ValidationError.raw() method.
Example:
class User(oblate.Schema):
id = fields.Integer()
username = fields.String()
is_employee = fields.Boolean(default=False)
try:
User({'id': 'invalid integer'})
except oblate.ValidationError as err:
print(err.raw())
Output:
{
'id': ['Value of this field must be an integer'],
'username': ['This field is required.']
}
Oblate’s default raw format is a dictionary in which keys are the field names and the value is the list of errors that were raised for that field.
This raw format can also be customized to implement your own format. This is done by subclassing the
ValidationError and overriding the raw() method:
class CustomValidationError(oblate.ValidationError):
def raw(self):
# your implementation here
...
Your implementation would make use of the ValidationError.errors list which includes the
FieldError which caused the validation failure.
After that, you can change the Global configuration so that your subclass is raised
instead of default ValidationError:
oblate.config.validation_error_cls = CustomValidationError
Attaching arbitrary state#
When a FieldError is raised, you can attach any state to it. This state can be any metadata
related to error which can be accessed later.
This is done by providing the state parameter to FieldError which can be of any type:
raise FieldError('error message', state={'some_key': 'value'})
Then you can access the state later at another point in your code. A common use case of this is when you are implementing your own raw format (see the heading above) and you want to include some extra data in it such as error codes.
Example:
ERR_CODE_USERNAME_TOO_SHORT = 1
ERR_CODE_PASSWORD_TOO_SHORT = 2
class User(oblate.Schema):
username = fields.String()
password = fields.String()
@validate.field(username)
def validate_username(self, value, ctx):
if len(value) < 5:
state = {'error_code': ERR_CODE_USERNAME_TOO_SHORT}
raise oblate.FieldError('Username must be more than 5 chars.', state=state)
@validate.field(password)
def validate_password(self, value, ctx):
if len(value) < 8:
state = {'error_code': ERR_CODE_PASSWORD_TOO_SHORT}
raise oblate.FieldError('Password must be more than 8 chars.', state=state)
try:
User({'username': 'test', 'password': 'test'})
except ValidationError as err:
for error in err.errors:
print(error.state)
Output:
{'error_code': 1}
{'error_code': 2}
Custom error messages#
Oblate allows customizing the default error messages. This is done by overriding the
fields.Field.format_error() method.
The first parameter to this method is an error code. An error code is used to indicate the type of error raised. All fields have the following error codes:
Other error codes are specific to each field and their documentation can be found in the relevant
field’s documentation. Error codes are detailed under each field. They are upper case class attributes
prefixed with ERR_.
The second parameter to format_error() is the ErrorContext instance
holding the contextual information about the error. The method should return a str or A
FieldError instance.
Example:
class Integer(fields.Integer):
def format_error(self, error_code, ctx):
if error_code == self.ERR_INVALID_DATATYPE:
return f'{ctx.get_value()!r} is not an integer'
class User(oblate.Schema):
id = Integer()
try:
User({'id': 'invalid'})
except oblate.ValidationError as err:
print(err.raw())
Output:
{'id': ["'invalid' is not an integer"]}
ErrorContext.get_value() returns the value that caused the error. In case of certain
error codes, the error doesn’t have a causative value. In this case, a ValueError is
raised. Currently, the only error that doesn’t have a value is ERR_FIELD_REQUIRED.