You want a route that accepts JSON and rejects anything malformed, whether a missing field or a wrong type, before your logic runs. In Marreta Lang you do this by attaching a schema to the request. There is no validation library to wire up and no guard clauses to write for type checks: the schema is the contract.
Every snippet below is taken from the project’s tested example suite, so it behaves exactly as shown.
Prerequisites
- A scaffolded project (
marreta init hello). - The Quickstart finished, so routes and
replyare familiar.
Describe the shape
A schema lists the fields you expect and their types:
export schema ItemPayload
name: string
active: booleanBoth fields are required. To make one optional, give it a trailing ?. More on
that below.
Attach it to the route
Use take <name> as <Schema> on the route line. Marreta validates the incoming
body against the schema before the route body runs:
route POST "/contracts/request-only" take payload as ItemPayload
reply 200, { received: payload.name, active: payload.active }If the request is missing name, or sends active as text, Marreta returns
422 Unprocessable Entity automatically and your code never executes. Inside
the route, payload is already typed and safe to use.
This page is about the request. To contract the response as well, by shaping it and stripping extra fields, see Shape a response.
The schema also documents the request
Marreta generates an OpenAPI (Swagger) document from your routes. Binding the body
as <Schema> makes the request appear there as a named, typed component with its
required fields, and it documents the automatic 422. A bare take payload with no
schema still accepts a body, but the request shows up as a free-form, untyped
object.
For a quick prototype, an unbound body is fine. For a product whose clients depend
on a stable contract, we recommend binding the request as <Schema>.
Optional and nested fields
Mark a field optional with ?. A field can also be typed as another schema,
which validates the nested object too:
export schema Address
street: string
city: string
zipcode: string
export schema ContactPayload
name: string
email: string
age?: integer
address: AddressContactPayload accepts a request with or without age, but always requires a
well-formed address.
Validate business rules, not just shape
A schema covers structure. For rules it cannot express, such as “billing is
required” or “items must be present”, use require ... else fail:
route POST "/doc/orders" take payload as OrderPayload
require payload.billing else fail 400, "billing address is required"
require payload.items else fail 400, "items are required"Schema validation returns 422. Your own fail returns whatever status you give
it. Use the schema for what the data is, and require for what your domain
allows.
Try it
marreta serve &
# A well-formed request is accepted:
curl -s -X POST http://localhost:8080/contracts/request-only \
-H 'content-type: application/json' \
-d '{"name":"Alice","active":true}'
# → { "received": "Alice", "active": true }
# A malformed one is rejected with 422, before your code runs:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8080/contracts/request-only \
-H 'content-type: application/json' \
-d '{"name":"Alice"}'The second call prints 422, because active is missing.
Result checkpoint
You should now have a route that accepts a typed request body, rejects malformed input with an automatic 422 before your logic runs, and exposes that contract in the generated OpenAPI document.
Common pitfalls
- A required field is absent. Fields without
?reject a missing value with 422. If a field is genuinely optional, mark it with?and handle the absent case (for example withmatchorpayload.field or default). - 422 vs. 400. Type and presence failures are validation (422) and are
automatic. Reserve
fail 400for your own business rules.
Next steps
- Shape a response: contract the response, not just the request.
- Types: every field type a schema can declare.