from marshmallow import (
Schema, fields, validates, validates_schema,
ValidationError, RAISE,
)
from marshmallow.validate import Length, Email, Equal
class SignupSchema(Schema):
class Meta:
unknown = RAISE
email = fields.Email(required=True, validate=Email())
password = fields.Str(
required=True,
load_only=True,
validate=Length(min=8, max=128),
)
confirm_password = fields.Str(required=True, load_only=True)
display_name = fields.Str(required=True, validate=Length(min=2, max=40))
terms_accepted = fields.Bool(
required=True,
validate=Equal(True, error="You must accept the terms."),
)
@validates("password")
def validate_password(self, value, **kwargs):
if value.isalnum():
raise ValidationError(
"Password must include a symbol or punctuation character."
)
if value.lower() == value or value.upper() == value:
raise ValidationError("Password must mix upper and lower case.")
@validates_schema
def check_passwords_match(self, data, **kwargs):
pw = data.get("password")
confirm = data.get("confirm_password")
if pw and confirm and pw != confirm:
raise ValidationError(
"Passwords do not match.", field_name="confirm_password"
)
from flask import jsonify
from marshmallow import ValidationError
from werkzeug.exceptions import BadRequest
def register_error_handlers(app):
@app.errorhandler(ValidationError)
def handle_validation_error(err):
return jsonify({"errors": err.messages}), 422
@app.errorhandler(BadRequest)
def handle_bad_request(err):
return (
jsonify({"errors": {"_body": "Request body must be valid JSON."}}),
400,
)
from flask import Blueprint, request, jsonify
from marshmallow import ValidationError
from .schemas import SignupSchema
from .models import User, db
bp = Blueprint("auth", __name__, url_prefix="/auth")
signup_schema = SignupSchema()
@bp.post("/signup")
def signup():
# force=True lets Marshmallow own the shape; a truly broken body
# raises BadRequest, handled globally as a 400.
payload = request.get_json(force=True, silent=False)
data = signup_schema.load(payload)
if User.query.filter_by(email=data["email"]).first():
raise ValidationError(
{"email": ["An account with this email already exists."]}
)
user = User(email=data["email"], display_name=data["display_name"])
user.set_password(data["password"])
db.session.add(user)
db.session.commit()
return jsonify({"id": user.id, "email": user.email}), 201
This snippet shows how a Flask signup endpoint turns raw JSON into a clean, validated payload using Marshmallow, and how it surfaces per-field errors back to the client in a predictable shape. The core idea is to separate validation from route logic: the schema owns the rules, the route owns the flow, and a single error handler owns the response format.
In schemas.py, SignupSchema declares each field with its constraints — email uses the built-in Email validator, password combines a Length check with a custom validate_password method decorated with @validates, and terms_accepted must equal True via Equal. Marshmallow accumulates all failures at once rather than stopping at the first, so a client sees every problem in one round trip. The @validates_schema method check_passwords_match demonstrates cross-field validation, attaching its error to confirm_password so the message lands on the right input. Meta.unknown = RAISE rejects unexpected keys, which keeps mass-assignment surprises out of the handler.
The ValidationError raised by Marshmallow carries a messages dict keyed by field name, which maps naturally to form fields on the frontend. In errors.py, the register_error_handlers function installs an app-level handler for ValidationError that returns HTTP 422 with a consistent {"errors": ...} envelope, so no individual route needs a try/except around load.
In auth.py, the signup view calls schema.load(request.get_json()); because the error handler is registered globally, a validation failure short-circuits into the 422 response automatically. Only valid, coerced data reaches the business logic, so the duplicate-email check and User creation operate on trusted values. Note the deliberate choice to raise a ValidationError for the taken email too, keeping all field errors — including database-level ones — in the same envelope the frontend already understands.
The trade-off is that schema logic lives apart from the model, which means keeping the two in sync; the payoff is testable, reusable validation and a uniform error contract. This pattern scales well: the same schema can be reused for updates with partial=True, and the error shape stays stable as the API grows.
Related snips
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :excerpt, :body, :published_at, :views, :likes_count, :comments_count
attribute :can_edit, if: :current_user_can_edit?
belongs_to :author, serializer: UserSummarySerializer
Serializers with ActiveModel::Serializers
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
class PostsController < ApplicationController
def index
posts = Post.for_feed.page(params[:page]).per(25)
render json: {
data: posts.map { |post| PostSerializer.new(post).as_json },
N+1 Proof Serialization with preloaded associations
Share this code
Here's the card — post it anywhere.