Schema & API Docs¶
Heaven doesn't just run your code; it understands it. By using schemas, you get instant validation, auto-generated documentation, and type safety, all powered by the incredibly fast msgspec.
Defining Schemas¶
A schema is a class that describes your data structure. Heaven exports Schema (a wrapper around msgspec.Struct) and Constraints (a wrapper around msgspec.Meta) to help you define validation rules.
from typing import Annotated
from heaven import Schema, Field
class User(Schema):
id: int
name: str
# 1. Bounds (Values vs Lengths)
# Use 'min'/'max' for numeric values (>=, <=)
age: Annotated[int, Field(min=18, max=100, desc="Must be legal age")]
# Use 'min_len'/'max_len' for sequence lengths
tags: Annotated[list[str], Field(min_len=1, desc="At least one tag required")]
# 2. Formats
# Built-in support for 'email', 'uuid', and 'slug'
email: Annotated[str, Field(format="email", example="ray@heaven.com")]
apikey: Annotated[str, Field(format="uuid", error_hint="Invalid API Key format")]
slug: Annotated[str, Field(format="slug")]
# 3. Steps (Multiples)
duration: Annotated[int, Field(step=15)]
# 4. Defaults (via msgspec.field)
is_active: bool = True
metadata: dict = Schema.Field(default_factory=dict)
The Field Helper¶
Heaven provides a smart Field() helper that simplifies validation. It returns msgspec.Meta constraints configured for your needs.
| Argument | Maps To (msgspec) | Description |
|---|---|---|
min / max | ge / le | Numeric constraints (>=, <=) |
min_len / max_len | min_length / max_length | Sequence length constraints |
step | multiple_of | Number must be a multiple of X |
format | pattern | Presets: "email", "uuid", "slug" |
desc | description | Field description for OpenAPI |
example | extra_json_schema | Example value for docs |
error_hint | extra_json_schema | Custom error message hint |
Advanced Patterns¶
Heaven's Field helper is designed to scale with your needs. It seamlessly integrates with msgspec's advanced features.
1. Nested Arrays & Matrices¶
You can apply constraints to the list itself AND the items within it.
class BatchUpload(Schema):
# The LIST must have 1-100 items.
# Each ITEM must be a string of length 3-50.
tags: Annotated[
list[Annotated[str, Field(min_len=3, max_len=50)]],
Field(min_len=1, max_len=100)
]
2. Complex Unions (Polymorphism)¶
Validate data that can be one of multiple types, each with its own rules.
class SearchQuery(Schema):
# ID can be a positive Integer OR a UUID String
id: Annotated[int, Field(min=1)] | Annotated[str, Field(format="uuid")]
3. Timezones & Dates¶
Native msgspec constraints (like tz) pass straight through Field.
from datetime import datetime
class Event(Schema):
# Enforce timezone-aware datetimes (rejects naive inputs)
start_time: Annotated[datetime, Field(tz=True)]
4. The Escape Hatch (Raw Access)¶
If you strictly prefer raw msgspec, you can bypass Field entirely.
from heaven import Constraints
class RawMetal(Schema):
# Pure msgspec code works 100% of the time
data: Annotated[bytes, Constraints(min_length=10)]
The Schema Registry¶
Instead of cluttering your handlers with decorators, Heaven uses a "Sidecar" pattern. You register schemas on the router's schema property.
# 0. You can mount schemas on subdomains e.g.
api = app.subdomain('api')
api.schema.POST(...)
# 1. Or on the default subdomain i.e. `www`
app.schema.POST('/users',
expects=User,
returns=User,
title="Create User",
summary="Creates a new user in the system"
)
# 2. Then in your route handler(s)
async def create_user(req, res, ctx):
# Validated 'User' object injected into `data` by heaven
user = req.data
# maybe some database logic?...
# Heaven auto-converts this back to JSON
res.body = user
# 3. Look ma, no decorators!
app.POST('/users', create_user)
Validation¶
When you register an expects schema, Heaven automatically:
- Validates the incoming JSON body against the schema.
- Aborts with
422 Unprocessable Entityif it's invalid (with a nice error message). - Populates
req.datawith the validated object.
Auto-Generated Docs (OpenAPI)¶
Heaven can generate a stunning interactive API reference website for you.
# Mount the docs at /docs
app.DOCS('/docs', title="My API", version="1.0.0")
Now visit http://localhost:8000/docs in your browser. You will see a beautiful Scalar UI where you can test your endpoints.
Advanced: Subdomains¶
You can mount docs on a specific subdomain.
app.DOCS('/docs', subdomain='api')
Advanced: Output Protection¶
You can also control how strict Heaven is about what you send back.
app.schema.GET('/users/:id',
returns=User,
protect=True, # Strip fields not in User schema
strict=True # Error 500 if a required field is missing
)
protect=True: Prevents data leaks. If your DB returnspassword_hashbut your Schema doesn't have it, it won't be sent.partial=True: Allows sending only a subset of fields (good for PATCH updates).
Pro Tip: Performance
Heaven's Schema is powered by msgspec, widely considered the fastest JSON library for Python. While standard features cover 99% of cases, you can leverage the full power of msgspec directly if you need e.g. zero-copy decoding or Struct caching.
Next: Yay!!! You know json kung fu. But how well? On to API Docs.