Skip to content

API Request, Payload & DTO validation

API Request, Payload & DTO validation

Schema validation for incoming API request can be done by using utility functions and decorators provided in frappe_utils

Example

Lets consider we have to make an API that accepts following persons information.

{
    "name": "Jon",
    "age": 18,
    "email": "jon@castlecraft.com",
    "addresses": [
        {
            "city": "Mumbai",
            "state": "MH",
            "zip_code": "400068"
        }
    ]
}

We create a whitelisted endpoint in frappe to accept this request


@frappe.whitelist(methods=["POST"])
def create_person():
    # your logic
    return True

There are few validation that we would we want to make sure before the API call reaches the business logic such as - All required fields are provided - Fields with email are valid emails - Fields length are as expected - Nested fields are not null - etc

For defining our schema we can use python classes with marshmallow

Note: If your not using frappe_utils as a part of your frappe site you would have to install marshmallow or add it to your respective project dependency.

Create a schema for your anticipated request

from marshmallow import Schema, fields


class AddressSchema(Schema):
    street = fields.Str(required=True, validate=lambda s: len(s) > 0)
    city = fields.Str(required=True, validate=lambda s: len(s) > 0)
    zip_code = fields.Str(required=True, validate=lambda s: len(s) > 0)


class PersonSchema(Schema):
    name = fields.Str(
        required=True,
    )
    age = fields.Int(required=True)
    email = fields.Email(required=True)
    addresses = fields.List(fields.Nested(AddressSchema), required=True)

We can now append this schema validation onto our whitelisted function

import frappe

from frappe_utils.request_validator.request_validator import request_validator

@frappe.whitelist(methods=["POST"])
@request_validator(PersonSchema)
def create_person(*args, **kwargs):
    # body = kwargs.get(RequestAttributes.body.value)

    # your logic
    return True

Above will make sure to validate incoming request with validation as per the schema definitions.

Note: Request body is always available as a part of kwargs and could be fetched as shown above.

Custom Validation

Lets consider a few cases - Custom error message mapping (default we give string message) - Have addition attributes as a part of kwargs

you can use provided config and overwrite options to achieve the same

Schema

Complete docs on writing custom validation

# Schema with custom validation and allowed unknown fields
class PersonSchemaCustom(Schema):
    name = fields.Str(required=True, validate=lambda s: len(s) > 0)
    age = fields.Int(required=True, validate=lambda n: 18 <= n <= 150)
    email = fields.Email(required=True)
    addresses = fields.List(fields.Nested(AddressSchema), required=True)

    # Custom validation for any field
    @validates_schema
    def validate_addresses(self, data, **kwargs):
        addresses = data.get("addresses")
        if not addresses or len(addresses) == 0:
            raise ValidationError("Addresses must not be empty")

    # To allow fields that are not defined in schema
    class Meta:
        unknown = True

Python Options


def my_custom_error_function(e):
    return "CUSTOM ERROR MESSAGE!"

@frappe.whitelist(methods=["POST"])
@request_validator(
    PersonSchemaCustom,
    {
        "validation_error_parser": my_custom_error_function,
        "request_attributes": [
            RequestAttributes.body,
            RequestAttributes.user,
            RequestAttributes.request,
            RequestAttributes.query,
        ],
    },
)
def create_person(*args, **kwargs):
    body = kwargs.get(RequestAttributes.body.value)
    user = kwargs.get(RequestAttributes.user.value)
    query = kwargs.get(RequestAttributes.query.value)

    # Full Request
    request = kwargs.get(RequestAttributes.request.value)

    return body, user, query

Overwrite options for request_validator

Value Defaults Details
validation_error_parser Returns readable error string Accepts a callable function that takes 1 argument of type ValidateError from Marshmallow, function provided to this option will be called when there is error in validation of request and whatever the function returns will be appended as the error for API response.

Current: {"status_code": 400, "error": "Field: name, Errors: Missing data for required field.\n.."}

Overwritten: {"status_code": 400, "error": "Your Custom Error"}
request_attributes [RequestAttributes.body] Provide options for any attributes that you would need and it would be appended to kwargs parameter.

Default body is available as in kwargs, you can append

RequestAttributes.user For getting session user

RequestAttributes.request For complete request

RequestAttributes.query For request query