Request Validation and Schemas¶
Verified status as of March 28, 2026.
Quick View¶
- Complexity: Intermediate
- Typical time: 30-40 minutes
- Outcome: predictable input contracts with clear
400/422errors
Example handler (Node, Python, Rust, PHP)¶
Path used in this guide: functions/orders/[id]/post.*
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
exports.handler = async (event, { id }) => {
const orderId = Number(id);
if (!Number.isInteger(orderId) || orderId < 1 || orderId > 999999) {
return { status: 422, body: { error: "id must be an integer between 1 and 999999" } };
}
const source = event.query?.source || "web";
if (source.length < 2 || source.length > 20) {
return { status: 422, body: { error: "source length must be between 2 and 20" } };
}
let body;
try { body = JSON.parse(event.body || "{}"); }
catch { return { status: 400, body: { error: "invalid JSON body" } }; }
if (!body.customer || typeof body.customer.name !== "string") {
return { status: 422, body: { error: "customer.name is required" } };
}
if (!Array.isArray(body.items) || body.items.length === 0) {
return { status: 422, body: { error: "items must be a non-empty array" } };
}
if (body.delivery_date && !ISO_DATE.test(body.delivery_date)) {
return { status: 422, body: { error: "delivery_date must use YYYY-MM-DD" } };
}
return { status: 201, body: { order_id: orderId, source, customer: body.customer, items: body.items, delivery_date: body.delivery_date || null, gift: Boolean(body.gift) } };
};
import json
import re
ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def handler(event, params):
order_id = int(params.get("id", 0))
if order_id < 1 or order_id > 999999:
return {"status": 422, "body": {"error": "id must be an integer between 1 and 999999"}}
query = event.get("query") or {}
source = query.get("source", "web")
if len(source) < 2 or len(source) > 20:
return {"status": 422, "body": {"error": "source length must be between 2 and 20"}}
try:
body = json.loads(event.get("body") or "{}")
except Exception:
return {"status": 400, "body": {"error": "invalid JSON body"}}
if not isinstance(body.get("customer"), dict) or not isinstance(body["customer"].get("name"), str):
return {"status": 422, "body": {"error": "customer.name is required"}}
if not isinstance(body.get("items"), list) or len(body["items"]) == 0:
return {"status": 422, "body": {"error": "items must be a non-empty array"}}
if body.get("delivery_date") and not ISO_DATE.match(body["delivery_date"]):
return {"status": 422, "body": {"error": "delivery_date must use YYYY-MM-DD"}}
return {"status": 201, "body": {"order_id": order_id, "source": source, "customer": body["customer"], "items": body["items"], "delivery_date": body.get("delivery_date"), "gift": bool(body.get("gift"))}}
use regex::Regex;
use serde_json::{json, Value};
pub fn handler(event: Value, params: Value) -> Value {
let order_id = params.get("id").and_then(|x| x.as_str()).and_then(|x| x.parse::<i64>().ok()).unwrap_or(0);
if !(1..=999999).contains(&order_id) {
return json!({"status": 422, "body": {"error": "id must be an integer between 1 and 999999"}});
}
let source = event.get("query").and_then(|q| q.get("source")).and_then(|x| x.as_str()).unwrap_or("web");
if source.len() < 2 || source.len() > 20 {
return json!({"status": 422, "body": {"error": "source length must be between 2 and 20"}});
}
let raw = event.get("body").and_then(|b| b.as_str()).unwrap_or("{}");
let body: Value = match serde_json::from_str(raw) {
Ok(v) => v,
Err(_) => return json!({"status": 400, "body": {"error": "invalid JSON body"}}),
};
if body.get("customer").and_then(|c| c.get("name")).and_then(|n| n.as_str()).is_none() {
return json!({"status": 422, "body": {"error": "customer.name is required"}});
}
if !body.get("items").map(|v| v.is_array()).unwrap_or(false) || body["items"].as_array().unwrap().is_empty() {
return json!({"status": 422, "body": {"error": "items must be a non-empty array"}});
}
if let Some(date) = body.get("delivery_date").and_then(|d| d.as_str()) {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
if !re.is_match(date) {
return json!({"status": 422, "body": {"error": "delivery_date must use YYYY-MM-DD"}});
}
}
json!({"status": 201, "body": {"order_id": order_id, "source": source, "customer": body["customer"], "items": body["items"], "delivery_date": body.get("delivery_date"), "gift": body.get("gift").and_then(|g| g.as_bool()).unwrap_or(false)}})
}
<?php
function handler(array $event, array $params): array {
$orderId = intval($params['id'] ?? 0);
if ($orderId < 1 || $orderId > 999999) {
return ['status' => 422, 'body' => ['error' => 'id must be an integer between 1 and 999999']];
}
$query = $event['query'] ?? [];
$source = $query['source'] ?? 'web';
if (strlen($source) < 2 || strlen($source) > 20) {
return ['status' => 422, 'body' => ['error' => 'source length must be between 2 and 20']];
}
$raw = $event['body'] ?? '{}';
$body = json_decode($raw, true);
if (!is_array($body)) return ['status' => 400, 'body' => ['error' => 'invalid JSON body']];
if (!isset($body['customer']['name']) || !is_string($body['customer']['name'])) {
return ['status' => 422, 'body' => ['error' => 'customer.name is required']];
}
if (!isset($body['items']) || !is_array($body['items']) || count($body['items']) === 0) {
return ['status' => 422, 'body' => ['error' => 'items must be a non-empty array']];
}
if (!empty($body['delivery_date']) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $body['delivery_date'])) {
return ['status' => 422, 'body' => ['error' => 'delivery_date must use YYYY-MM-DD']];
}
return ['status' => 201, 'body' => ['order_id' => $orderId, 'source' => $source, 'customer' => $body['customer'], 'items' => $body['items'], 'delivery_date' => $body['delivery_date'] ?? null, 'gift' => (bool)($body['gift'] ?? false)]];
}
Validation curls (per runtime)¶
curl -sS -X POST 'http://127.0.0.1:8080/orders/7?source=w' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/0' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7' -H 'Content-Type: application/json' -d '{"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7?source=w' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/0' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7' -H 'Content-Type: application/json' -d '{"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7?source=w' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/0' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7' -H 'Content-Type: application/json' -d '{"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7?source=w' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/0' -H 'Content-Type: application/json' -d '{"customer":{"name":"Ana"},"items":[{"sku":"A1","qty":1}]}'
curl -sS -X POST 'http://127.0.0.1:8080/orders/7' -H 'Content-Type: application/json' -d '{"items":[{"sku":"A1","qty":1}]}'
Path + body contract¶
| Field | Source | Required | Type | Rule |
|---|---|---|---|---|
id |
path | yes | integer | 1..999999 |
source |
query | no | string | default web, len 2..20 |
customer.name |
body | yes | string | non-empty |
items |
body | yes | array | at least 1 element |
delivery_date |
body | no | string | YYYY-MM-DD |
Validation checklist¶
- Invalid JSON returns
400 - Contract violations return
422 - Valid payload returns
201 - Endpoint appears in OpenAPI at
/openapi.json
Related links¶
Last reviewed:
March 28, 2026
·
Docs on fastfn.dev