Schemas¶
Schemas do two jobs: validate incoming data and serialize outgoing resources. Each backend generates one from the model or table, and you can replace it or extend it.
The generated schema¶
Meta.Schema is created when you do not provide one. The name is
{Meta.name.title()}Schema, and the base class comes from the backend:
| Backend | Base | Fields come from |
|---|---|---|
RESTHandler |
marshmallow.Schema |
schema_fields only |
| Peewee | marshmallow_peewee.ModelSchema |
the model |
| SQLAlchemy | SQLAlchemyAutoSchema |
the table |
| Mongo | MongoSchema |
_id plus schema_fields |
A plain RESTHandler has no model, so its generated schema has no fields. Add
them with schema_fields or a custom Schema.
class Pets(RESTHandler):
class Meta:
name = "pet"
schema_fields = {
"name": ma.fields.String(required=True),
"age": ma.fields.Integer(),
}
For Mongo, the same pattern is the normal case:
class Pets(MongoRESTHandler):
class Meta:
collection = pets
schema_fields = {
"name": ma.fields.String(required=True),
"status": ma.fields.String(),
}
Custom schemas¶
Pass a schema class in Meta:
class PetSchema(ma.Schema):
name = ma.fields.String(required=True)
status = ma.fields.String(load_default="available")
class Pets(PWRESTHandler):
class Meta:
model = Pet
Schema = PetSchema
The schema is used for both dump and load. It also becomes the OpenAPI
component for the resource.
A schema can be nested inside Meta:
class Pets(RESTHandler):
class Meta:
name = "pet"
class Schema(ma.Schema):
name = ma.fields.String(required=True)
Schema options¶
schema_base, schema_fields, and schema_meta control the generated schema
without writing one:
class Pets(PWRESTHandler):
class Meta:
model = Pet
schema_base = MyModelSchema
schema_fields = {"extra": ma.fields.String()}
schema_meta = {"dump_only": ("created_at",)}
schema_unknown = ma.EXCLUDE
schema_unknown maps to Marshmallow's unknown option. The default is
EXCLUDE, so unknown request fields are dropped silently. Use INCLUDE to keep
them or RAISE to reject them.
schema_meta is merged into the generated Meta. For Peewee and SQLAlchemy it
can override generated settings such as model, table, include_fk, or
dump_only. For Mongo it only carries options understood by MongoSchema.
Validation¶
Incoming data is validated by load. A validation error returns 400 with the
Marshmallow messages:
{
"error": true,
"message": "Bad request data",
"errors": {"name": ["Missing data for required field."]}
}
Invalid JSON returns 400 with the parser message and no errors key.
On create, required fields must be present. On update (PUT, or POST with an
id), the schema is loaded with partial=True, so missing fields keep their
current values.
SQLAlchemy fills column defaults on create before the insert. Peewee and Mongo use their own defaults.
Reading and writing data¶
The default pipeline is:
request body -> load_data -> schema.load -> save -> schema.dump -> response
Override load_data when the body is not plain JSON:
async def load_data(self, request):
return await request.form()
Override load to change validation options or post-process the result:
async def load(self, request, resource=None, **options):
data = await super().load(request, resource=resource, **options)
data["updated_by"] = self.auth
return data
Override dump to add computed fields or reshape the response:
async def dump(self, request, data, *, many=False):
result = await super().dump(request, data, many=many)
if many:
for item in result:
item["url"] = f"/api/pet/{item['id']}"
else:
result["url"] = f"/api/pet/{result['id']}"
return result
get_schema is the single place where the schema is instantiated. Override it
to add constructor options:
def get_schema(self, request, *, resource=None, **options):
options.setdefault("context", {"request": request})
return super().get_schema(request, resource=resource, **options)
Field selection¶
schema_only and schema_exclude are applied inside get_schema:
GET /api/pet?schema_only=id,name
GET /api/pet?schema_exclude=password
Because the same schema instance is used for input and output, the selection
affects both. This trims responses and limits what a request body can write,
but it is not access control: the client chooses the query parameter. Enforce
permissions in authorize or in the write hooks.
Enum fields¶
muffin_rest.schemas.EnumField serializes an enum by value and validates on
deserialize:
import enum
from muffin_rest.schemas import EnumField
class Status(enum.Enum):
AVAILABLE = "available"
SOLD = "sold"
class PetSchema(ma.Schema):
status = EnumField(Status)
Pass by_value=False to use enum names instead of values. Peewee fields from
muffin_peewee (StrEnumField, IntEnumField) are converted to this field
automatically.
Backend notes¶
Peewee schemas can return model instances from post_load, which is useful for
creating related records. SQLAlchemy and Mongo schemas merge the loaded data
into the existing row or document on update. See Backends for
the exact behavior.