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:

  • ValueError

  • AssertionError

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.