Skip to main content

Bring Pydantic's power to Marshmallow: type annotations, validators, and automatic schema generation

Project description

pydantic-marshmallow

CI PyPI version Python 3.10+ License: MIT

Bridge Pydantic's power with Marshmallow's ecosystem. Use Pydantic models for validation with full Marshmallow compatibility.

📖 Documentation | 🐙 GitHub

Why pydantic-marshmallow?

Get the best of both worlds: Pydantic's speed with Marshmallow's ecosystem.

Performance

pydantic-marshmallow uses Pydantic's Rust-powered validation engine under the hood, delivering significant performance improvements over native Marshmallow—especially for nested data structures.

Performance Comparison

Operation MA 3.x MA 4.x Bridge (MA 3.x) Bridge (MA 4.x)
Simple load 5.3 µs 4.8 µs 2.9 µs 2.9 µs
Nested model 10.9 µs 10.5 µs 3.4 µs 3.2 µs
Deep nested (4 levels) 31.2 µs 29.0 µs 5.3 µs 5.2 µs
Batch (100 items) 454 µs 424 µs 260 µs 259 µs

What the Numbers Show

  1. MA 3.x → MA 4.x: Marshmallow 4.x improved ~10% over 3.x (5.3 → 4.8 µs for simple loads)
  2. MA 3.x → Bridge: Adding Pydantic delivers 1.8x–5.9x speedup over pure MA 3.x
  3. MA 4.x → Bridge: Still 1.6x–5.6x faster than native MA 4.x
  4. Bridge consistency: ~2.9 µs for simple loads regardless of Marshmallow version

The bridge delegates validation to Pydantic's Rust-powered core, bypassing Marshmallow's field processing. This means bridge performance is independent of Marshmallow version—you get consistent speed whether you're on MA 3.x or 4.x.

Benchmarks: Python 3.11, median of 3 runs with IQR outlier removal. Run python -m benchmarks.run_benchmarks to reproduce.

Why it matters

  • Existing Marshmallow projects: Incrementally adopt Pydantic validation without rewriting your API layer
  • Flask/webargs/apispec users: Keep your integrations, get faster validation
  • Performance-sensitive APIs: Nested model validation is 3-6x faster than native Marshmallow

Features

  • Pydantic Validation: Leverage Pydantic's Rust-powered validation engine
  • Marshmallow Compatibility: Works with Flask-Marshmallow, webargs, apispec, SQLAlchemy, and more
  • Zero Drift: Single source of truth - Pydantic model defines the schema
  • Full Hook Support: @pre_load, @post_load, @pre_dump, @post_dump
  • Validators: @validates("field") and @validates_schema decorators
  • Partial Loading: partial=True or partial=('field1', 'field2')
  • Unknown Fields: unknown=RAISE/EXCLUDE/INCLUDE
  • Computed Fields: Pydantic @computed_field support in serialization

Installation

pip install pydantic-marshmallow

Requirements: Python 3.10+, Pydantic 2.0+, Marshmallow 3.18+ (including 4.x)

Quick Start

Basic Usage

from pydantic import BaseModel, EmailStr, Field
from pydantic_marshmallow import PydanticSchema

class User(BaseModel):
    name: str = Field(min_length=1)
    email: EmailStr
    age: int = Field(ge=0)

class UserSchema(PydanticSchema[User]):
    class Meta:
        model = User

# Use like any Marshmallow schema
schema = UserSchema()
user = schema.load({"name": "Alice", "email": "alice@example.com", "age": 30})
print(user.name)  # "Alice" - it's a Pydantic User instance!

# Serialize back
data = schema.dump(user)
# {"name": "Alice", "email": "alice@example.com", "age": 30}

Using the Decorator

from pydantic_marshmallow import pydantic_schema

@pydantic_schema
class User(BaseModel):
    name: str
    email: EmailStr

# .Schema attribute is automatically added
schema = User.Schema()
user = schema.load({"name": "Alice", "email": "alice@example.com"})

Factory Function

from pydantic_marshmallow import schema_for

UserSchema = schema_for(User)
schema = UserSchema()

Marshmallow Hooks

All standard Marshmallow hooks work:

from marshmallow import pre_load, post_load, validates

class UserSchema(PydanticSchema[User]):
    class Meta:
        model = User

    @pre_load
    def normalize_email(self, data, **kwargs):
        if "email" in data:
            data["email"] = data["email"].lower().strip()
        return data

    @post_load
    def log_user(self, user, **kwargs):
        print(f"Loaded user: {user.name}")
        return user

    @validates("name")
    def validate_name(self, value):
        if value.lower() == "admin":
            raise ValidationError("Cannot use 'admin' as name")

Partial Loading

# Allow all missing required fields
user = schema.load({"name": "Alice"}, partial=True)

# Allow specific missing fields
user = schema.load({"name": "Alice"}, partial=("email", "age"))

Unknown Field Handling

from marshmallow import EXCLUDE, INCLUDE, RAISE

# Reject unknown fields (default)
schema = UserSchema(unknown=RAISE)

# Ignore unknown fields
schema = UserSchema(unknown=EXCLUDE)

# Include unknown fields in result
schema = UserSchema(unknown=INCLUDE)

Field Filtering

# Only include specific fields
schema = UserSchema(only=("name", "email"))

# Exclude specific fields
schema = UserSchema(exclude=("age",))

# Load-only fields (not in dump output)
schema = UserSchema(load_only=("password",))

# Dump-only fields (not in load input)
schema = UserSchema(dump_only=("created_at",))

Computed Fields

from pydantic import computed_field

class User(BaseModel):
    first: str
    last: str

    @computed_field  # type: ignore[misc]
    @property
    def full_name(self) -> str:
        return f"{self.first} {self.last}"

schema = schema_for(User)()
user = User(first="Alice", last="Smith")
data = schema.dump(user)
# {"first": "Alice", "last": "Smith", "full_name": "Alice Smith"}

Dump Options

# Exclude None values
schema.dump(user, exclude_none=True)

# Exclude unset fields
schema.dump(user, exclude_unset=True)

# Exclude fields with default values
schema.dump(user, exclude_defaults=True)

JSON Serialization

Direct JSON string support:

# Deserialize from JSON string
user = schema.loads('{"name": "Alice", "email": "alice@example.com", "age": 30}')

# Serialize to JSON string  
json_str = schema.dumps(user)

# Batch operations
users = schema.loads('[{"name": "Alice", ...}, {"name": "Bob", ...}]', many=True)

Flask-Marshmallow Integration

from flask import Flask
from flask_marshmallow import Marshmallow
from pydantic_marshmallow import schema_for

app = Flask(__name__)
ma = Marshmallow(app)

UserSchema = schema_for(User)

@app.route("/users", methods=["POST"])
def create_user():
    schema = UserSchema()
    user = schema.load(request.json)
    # user is a Pydantic User instance
    return schema.dump(user)

webargs Integration

from webargs.flaskparser import use_args
from pydantic_marshmallow import schema_for

UserSchema = schema_for(User)

@app.route("/users", methods=["POST"])
@use_args(UserSchema(), location="json")
def create_user(user):
    # user is a Pydantic User instance
    return {"message": f"Created {user.name}"}

apispec Integration

from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin

spec = APISpec(
    title="My API",
    version="1.0.0",
    openapi_version="3.0.0",
    plugins=[MarshmallowPlugin()],
)

spec.components.schema("User", schema=UserSchema)

flask-smorest Integration

Build REST APIs with automatic OpenAPI documentation:

from flask import Flask
from flask_smorest import Api, Blueprint
from pydantic import BaseModel, Field
from pydantic_marshmallow import schema_for

app = Flask(__name__)
app.config["API_TITLE"] = "My API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"

api = Api(app)
blp = Blueprint("users", __name__, url_prefix="/users")

class UserCreate(BaseModel):
    name: str = Field(min_length=1)
    email: str

UserCreateSchema = schema_for(UserCreate)
UserSchema = schema_for(User)

@blp.post("/")
@blp.arguments(UserCreateSchema)
@blp.response(201, UserSchema)
def create_user(data):
    # data is a Pydantic UserCreate instance
    user = User(id=1, name=data.name, email=data.email)
    return UserSchema().dump(user)

api.register_blueprint(blp)

flask-rebar Integration

Build REST APIs with automatic Swagger documentation:

from flask import Flask
from flask_rebar import Rebar, get_validated_body
from pydantic_marshmallow import schema_for

app = Flask(__name__)
rebar = Rebar()
registry = rebar.create_handler_registry()

UserCreateSchema = schema_for(UserCreate)
UserSchema = schema_for(User)

@registry.handles(
    rule="/users",
    method="POST",
    request_body_schema=UserCreateSchema(),
    response_body_schema=UserSchema(),
)
def create_user():
    data = get_validated_body()  # Pydantic UserCreate instance
    user = User(id=1, name=data.name, email=data.email)
    return UserSchema().dump(user)

rebar.init_app(app)

SQLAlchemy Pattern

Use Pydantic for API validation alongside SQLAlchemy ORM:

from sqlalchemy.orm import Session
from pydantic_marshmallow import schema_for

# Pydantic model for API validation
class UserCreate(BaseModel):
    name: str = Field(min_length=1)
    email: str

UserCreateSchema = schema_for(UserCreate)

def create_user(session: Session, data: dict):
    # Validate API input with Pydantic
    schema = UserCreateSchema()
    validated = schema.load(data)  # Returns Pydantic model
    
    # Create ORM object from validated data
    orm_user = UserModel(name=validated.name, email=validated.email)
    session.add(orm_user)
    session.commit()
    return orm_user

HybridModel

For models that need both Pydantic and Marshmallow APIs:

from pydantic_marshmallow import HybridModel

class User(HybridModel):
    name: str
    email: EmailStr

# Use as Pydantic model
user = User(name="Alice", email="alice@example.com")

# Use Marshmallow-style loading
user = User.ma_load({"name": "Alice", "email": "alice@example.com"})

# Get the Marshmallow schema class
schema_class = User.marshmallow_schema()

Error Handling

Validation errors are raised as BridgeValidationError, which extends Marshmallow's ValidationError:

from pydantic_marshmallow import BridgeValidationError

try:
    user = schema.load({"name": "", "email": "invalid"})
except BridgeValidationError as e:
    print(e.messages)
    # {'name': ['String should have at least 1 character'],
    #  'email': ['value is not a valid email address']}
    print(e.valid_data)
    # {} - fields that passed validation

API Reference

PydanticSchema

A Marshmallow schema backed by a Pydantic model.

Class Methods:

  • from_model(model, **meta_options) - Create a schema class from a Pydantic model

Instance Methods:

  • load(data, *, many, partial, unknown, return_instance) - Deserialize data
  • dump(obj, *, many, exclude_none, exclude_unset, exclude_defaults) - Serialize data
  • validate(data, *, many, partial) - Validate without deserializing

Hooks:

  • on_bind_field(field_name, field_obj) - Called when a field is bound
  • handle_error(error, data, *, many) - Custom error handling

Factory Functions

  • schema_for(model, **meta_options) - Create a schema class from a model
  • pydantic_schema - Decorator that adds .Schema to a model

HybridModel

A Pydantic model with built-in Marshmallow support.

Class Methods:

  • marshmallow_schema() - Get the Marshmallow schema class
  • ma_load(data, **kwargs) - Load using Marshmallow
  • ma_loads(json_str, **kwargs) - Load from JSON string

Instance Methods:

  • ma_dump(**kwargs) - Dump using Marshmallow
  • ma_dumps(**kwargs) - Dump to JSON string

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_marshmallow-1.1.0.tar.gz (277.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_marshmallow-1.1.0-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_marshmallow-1.1.0.tar.gz.

File metadata

  • Download URL: pydantic_marshmallow-1.1.0.tar.gz
  • Upload date:
  • Size: 277.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pydantic_marshmallow-1.1.0.tar.gz
Algorithm Hash digest
SHA256 f43d90f2797eaf9083c5f938d896d0523996fb781fe1d50a0390b5f83e714a5f
MD5 c0c1eb95b1d201433a10829400bf29a3
BLAKE2b-256 3bea029e85fd2e273a96f5642252775411e165114a687c03ffacfd6601ee8b99

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydantic_marshmallow-1.1.0.tar.gz:

Publisher: release.yml on mockodin/pydantic-marshmallow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pydantic_marshmallow-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_marshmallow-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5cbdd66e2c90901a7a9bac53777c1481149588fb2aa6e7309a49fd302a55724d
MD5 e47bee07b1d380d5676d2de9ac4284d6
BLAKE2b-256 735955049bbec82119b3abb44dd87c61b250782d39f7081d65ca54381fc79c49

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydantic_marshmallow-1.1.0-py3-none-any.whl:

Publisher: release.yml on mockodin/pydantic-marshmallow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page