Skip to content

Validation

LightAPI validates request bodies automatically using Pydantic v2. Constraints are declared directly on field annotations via Field(...) and are enforced on every POST, PUT, and PATCH request.

Field constraints

from typing import Optional
from decimal import Decimal
from lightapi import RestEndpoint, Field

class ProductEndpoint(RestEndpoint):
    name: str = Field(min_length=1, max_length=100)
    slug: str = Field(min_length=1, max_length=100, unique=True, index=True)
    price: Decimal = Field(gt=0, decimal_places=2)
    stock: int = Field(ge=0, default=0)
    description: Optional[str] = Field(None, max_length=2000)

All standard Pydantic v2 field constraints are supported.

Validation errors

When validation fails, LightAPI returns 422 Unprocessable Entity with a detailed Pydantic error body:

{
  "detail": [
    {
      "type": "string_too_short",
      "loc": ["name"],
      "msg": "String should have at least 1 character",
      "input": "",
      "ctx": {"min_length": 1}
    },
    {
      "type": "greater_than",
      "loc": ["price"],
      "msg": "Input should be greater than 0",
      "input": -5,
      "ctx": {"gt": 0}
    }
  ]
}

Supported field types

Python type SQLAlchemy column Notes
str String
int Integer
float Float
bool Boolean
Decimal Numeric decimal_places=N caps the places a value may carry
datetime.datetime DateTime
Optional[T] nullable column Makes the column nullable

Field() extra kwargs

Beyond standard Pydantic constraints, LightAPI adds extra kwargs processed by the metaclass:

Kwarg Type Description
unique=True bool Adds a UNIQUE constraint to the column.
index=True bool Creates a database index on the column.
foreign_key="table.col" str Creates a foreign key reference.
decimal_places=N int A Decimal with more than N decimal places is rejected (422); the column type stays NUMERIC.
exclude=True bool Skips column creation entirely — field exists only on the schema.
default=<value> any Sets both the Pydantic default and the SQLAlchemy column default.

Custom validation via method overrides

Override post, put, or patch to add domain-level validation:

import json
from lightapi import RestEndpoint, Field

class UserEndpoint(RestEndpoint):
    username: str = Field(min_length=3, max_length=50)
    email: str

    async def post(self, request):
        data = json.loads(await request.body())
        if not data.get("email", "").endswith("@mycompany.com"):
            from starlette.responses import JSONResponse
            return JSONResponse(
                {"detail": "Only @mycompany.com emails are allowed"},
                status_code=422,
            )
        return await self._create_async(data)

Auto-injected columns

These columns are always present and never need to be declared:

Column Type Notes
id Integer (PK, autoincrement) Never writeable
created_at DateTime Set on insert
updated_at DateTime Updated on every write
version Integer Optimistic locking counter — must be included in PUT/PATCH bodies

Optimistic locking

version prevents lost updates. Every PUT and PATCH request must include the current version value. If it doesn't match the database, the request returns 409 Conflict.

# Create
curl -X POST /items -d '{"name": "Widget"}' -H "Content-Type: application/json"
# → 201 {"id": 1, "name": "Widget", "version": 1, ...}

# Update — include current version
curl -X PUT /items/1 -d '{"name": "Widget Pro", "version": 1}' -H "Content-Type: application/json"
# → 200 {"id": 1, "name": "Widget Pro", "version": 2, ...}

# Stale update — wrong version
curl -X PUT /items/1 -d '{"name": "Widget Max", "version": 1}' -H "Content-Type: application/json"
# → 409 {"detail": "Version conflict"}

Serializer and read-only fields

Use Meta.serializer to control which fields appear in responses (read) vs. what is accepted in request bodies (write):

from lightapi import RestEndpoint, Serializer

class ProfileEndpoint(RestEndpoint):
    username: str
    email: str
    hashed_password: str = Field(exclude=True)  # never in schema

    class Meta:
        serializer = Serializer(
            read=["id", "username", "email", "created_at"],
            write=["username", "email"],
        )

See API Reference — REST for the full Serializer reference.