Skip to main content

Generate PlantUML ERD diagrams from SQLModel, SQLAlchemy, Pydantic and dataclass models

Project description

๐Ÿ—ƒ๏ธ erdify

PyPI version Python versions License: MIT Tests Linting Ruff Checked with mypy

๐Ÿš€ Generate beautiful PlantUML Entity Relationship Diagrams from your SQLModel, SQLAlchemy, Pydantic and dataclass models automatically!

erdify parses your model files using AST (Abstract Syntax Tree) and generates comprehensive ERD diagrams in PlantUML format. It supports SQLModel, SQLAlchemy 2.0, Pydantic and standard-library dataclasses. No database connection required!

โœจ Features

  • ๐Ÿ“Š Automatic ERD Generation - Parse your models and generate PlantUML diagrams
  • ๐Ÿงฌ 4 Frameworks - SQLModel, SQLAlchemy 2.0 (Mapped[...]/mapped_column()), Pydantic and dataclasses
  • ๐Ÿ” AST-Based Parsing - No imports needed, works with any valid Python code
  • ๐ŸŽฏ Zero Runtime Dependencies - Uses only Python standard library
  • ๐Ÿ”— Relationship Detection - Automatically detects foreign keys and relationships
  • ๐Ÿ”‘ Key Inference - Optional --infer-keys derives PK/FK from field names for keyless models
  • ๐Ÿšซ Exclude Patterns - Filter out entities by class or table name with glob patterns
  • ๐Ÿ“ฆ Inheritance Support - Correctly resolves fields from base classes and mixins
  • ๐Ÿท๏ธ Enum Support - Includes enum definitions in the diagram
  • ๐Ÿ”„ Link Table Detection - Identifies many-to-many relationship tables
  • ๐ŸŽจ Beautiful Output - Clean, readable PlantUML with proper styling

๐Ÿ“ฆ Installation

Using pip

pip install erdify

Using uv

uv add erdify

Using pipx (recommended for CLI usage)

pipx install erdify

Using uvx (no installation needed)

# Run directly without installing
uvx erdify ./src/database -o erd.puml

๐Ÿš€ Quick Start

Command Line

# Generate ERD from a models directory
erdify ./src/database -o erd.puml

# With custom title
erdify ./src/models --title "My Database Schema" -o schema.puml

# Output to stdout
erdify ./src/database

Python API

from pathlib import Path
from erdify import parse_models_directory, generate_plantuml

# Parse your models
entities, enums = parse_models_directory(Path("./src/database"))

# Generate PlantUML
diagram = generate_plantuml(
    entities=entities,
    enums=enums,
    title="My Database ERD"
)

# Save or use the diagram
Path("erd.puml").write_text(diagram)

๐Ÿ“– Usage

CLI Options

usage: erdify [-h] [-o OUTPUT] [--title TITLE] [--exclude [PATTERN ...]]
                    [--infer-keys] [--no-enums] [--no-relationships] [-v]
                    input

Generate PlantUML ERD diagrams from SQLModel, SQLAlchemy, Pydantic and dataclass models

positional arguments:
  input                 Directory containing model files (searches for models.py recursively)

options:
  -h, --help            show this help message and exit
  -o OUTPUT, --output OUTPUT
                        Output .puml file (default: stdout)
  --title TITLE         Diagram title (default: 'Database ERD')
  --exclude [PATTERN ...]
                        Glob patterns (case-sensitive) to exclude entities by
                        class name or table name, e.g. --exclude '*Link' audit_log
  --infer-keys          For keyless models (Pydantic/dataclass), infer a primary
                        key from a field named 'id' and a foreign key from '<x>_id'
  --no-enums            Skip enum definitions in output
  --no-relationships    Skip relationship lines in output
  -v, --version         show program's version number and exit

Excluding Entities

Use --exclude to drop tables/entities from the diagram. Each pattern is a case-sensitive glob tested against both the class name and the table name โ€” an entity is excluded if either matches. Any relationships pointing at an excluded entity are dropped too, so no dangling lines remain.

# Exclude all link tables (class names ending in "Link")
erdify ./src/database --exclude '*Link'

# Exclude by table name, with multiple patterns
erdify ./src/database --exclude audit_log 'tmp_*' Session

๐Ÿ’ก Quote patterns containing * so your shell doesn't expand them.

Running as Module

python -m erdify ./src/database -o erd.puml

Example Models

Given these SQLModel definitions:

from enum import Enum
from sqlmodel import SQLModel, Field, Relationship

class UserRole(Enum):
    ADMIN = "admin"
    USER = "user"

class User(SQLModel, table=True):
    __tablename__: str = "user"

    id: int = Field(primary_key=True)
    name: str
    email: str = Field(index=True)
    role: UserRole = Field(default=UserRole.USER)

    orders: list["Order"] = Relationship(back_populates="user")

class Order(SQLModel, table=True):
    __tablename__: str = "order"

    id: int = Field(primary_key=True)
    user_id: int = Field(foreign_key="user.id")
    total: float

    user: "User" = Relationship(back_populates="orders")

The tool generates:

Example ERD Image

with following code:

@startuml Database ERD
!define primary_key(x) <b><color:#b8861b><&key></color> x</b>
!define foreign_key(x) <color:#aaaaaa><&key></color> x
!define column(x) <color:#efefef><&media-record></color> x

skinparam linetype ortho

' Enums
enum UserRole << (E,#FFCC00) >> {
  ADMIN
  USER
}

' Entities
entity "user" as User {
  primary_key(id) : int
  column(name) : str
  column(email) : str
  column(role) : UserRole = USER
}

entity "order" as Order {
  primary_key(id) : int
  foreign_key(user_id) : int
  column(total) : float
}

' Relationships
Order }o--|| User : "user_id"

@enduml

๐Ÿงฌ One Schema, Four Frameworks

erdify supports four model frameworks. The snippets below all describe the same User / Order schema โ€” only the syntax differs. Each one produces the identical diagram:

Framework comparison ERD

โ„น๏ธ The SQLModel and SQLAlchemy versions declare keys explicitly. Pydantic and dataclasses have no key concept, so they are rendered with --infer-keys (id โ†’ PK, <x>_id โ†’ FK) to match. The runnable sources live in docs/examples/.

SQLModelSQLAlchemy 2.0
from sqlmodel import (
    Field, Relationship, SQLModel,
)


class User(SQLModel, table=True):
    __tablename__: str = "user"
    id: int = Field(primary_key=True)
    name: str
    email: str
    orders: list["Order"] = Relationship(
        back_populates="user")


class Order(SQLModel, table=True):
    __tablename__: str = "order"
    id: int = Field(primary_key=True)
    user_id: int = Field(
        foreign_key="user.id")
    total: float
    user: "User" = Relationship(
        back_populates="orders")
from sqlalchemy import ForeignKey
from sqlalchemy.orm import (
    DeclarativeBase, Mapped,
    mapped_column, relationship,
)


class Base(DeclarativeBase): ...


class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(
        primary_key=True)
    name: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()
    orders: Mapped[list["Order"]] = (
        relationship(back_populates="user"))


class Order(Base):
    __tablename__ = "order"
    id: Mapped[int] = mapped_column(
        primary_key=True)
    user_id: Mapped[int] = mapped_column(
        ForeignKey("user.id"))
    total: Mapped[float] = mapped_column()
    user: Mapped["User"] = relationship(
        back_populates="orders")
Pydantic --infer-keysDataclass --infer-keys
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str
    email: str
    orders: list["Order"] = []


class Order(BaseModel):
    id: int
    user_id: int
    total: float
    user: "User"
from dataclasses import dataclass, field


@dataclass
class User:
    id: int
    name: str
    email: str
    orders: list["Order"] = field(
        default_factory=list)


@dataclass
class Order:
    id: int
    user_id: int
    total: float
    user: "User" = None

How each framework is detected & parsed:

Framework Detected by Keys Relationships
SQLModel table=True Field(primary_key=โ€ฆ, foreign_key=โ€ฆ) Relationship()
SQLAlchemy 2.0 __tablename__ + Mapped[...] columns mapped_column(primary_key=โ€ฆ), ForeignKey(...) relationship() (lowercase)
Pydantic BaseModel subclass (incl. transitive) --infer-keys only nested model refs (user: User, list["Order"])
Dataclass @dataclass decorator --infer-keys only nested model refs

โ„น๏ธ Mixins / abstract bases (e.g. a SQLAlchemy mixin without __tablename__) are not drawn as tables, but their columns are inherited into concrete entities. Imports aliased to other names (e.g. mapped_column as mc) are not detected.

Inferring keys (--infer-keys)

Pydantic models and dataclasses have no database key concept. By default all fields are rendered as plain columns and relationships come only from nested model references. If your models follow a database-like naming convention, pass --infer-keys to derive keys from field names:

  • a field named id โ†’ primary key
  • a field named <x>_id โ†’ foreign key targeting table <x>
# Plain columns (default)
erdify ./src/schemas

# Infer PK/FK from id / <x>_id naming
erdify ./src/schemas --infer-keys

โ„น๏ธ --infer-keys only affects Pydantic/dataclass models. SQLModel and SQLAlchemy keys are always read from the explicit definitions and never overridden.

๐ŸŽจ Viewing the Diagram

Online

  1. Copy the generated .puml content
  2. Paste at PlantUML Web Server

Local with PlantUML

# Install PlantUML (macOS)
brew install plantuml

# Generate PNG
plantuml erd.puml

# Generate SVG
plantuml -tsvg erd.puml

VS Code Extension

Install the PlantUML extension for live preview.

๐Ÿ”ง Advanced Usage

Programmatic Access

from erdify import (
    ASTDatabaseParser,
    PlantUMLGenerator,
    EntityInfo,
    FieldInfo,
    EnumInfo,
)

# Low-level parser access
parser = ASTDatabaseParser(Path("./models"))
entities, enums = parser.parse_all_models()

# Access entity details
for name, entity in entities.items():
    print(f"Table: {entity.table_name}")
    for field in entity.fields:
        if field.is_primary_key:
            print(f"  PK: {field.name}")
        elif field.is_foreign_key:
            print(f"  FK: {field.name} -> {field.foreign_table}")

# Custom generator with options
generator = PlantUMLGenerator(
    entities=entities,
    enums=enums,
    title="Custom ERD"
)
output = generator.generate()

Integration with CI/CD

# .github/workflows/docs.yml
name: Generate ERD

on:
  push:
    paths:
      - 'src/database/**'

jobs:
  generate-erd:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install erdify
        run: pip install erdify

      - name: Generate ERD
        run: erdify ./src/database --title "Database Schema" -o docs/erd.puml

      - name: Generate PNG
        run: |
          sudo apt-get install -y plantuml
          plantuml docs/erd.puml

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "docs: update ERD diagram"
          file_pattern: "docs/erd.*"

Integration with pre-commit hooks

Keep your ERD diagrams automatically updated on every commit using pre-commit:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: generate-erd
        name: ๐Ÿ—ƒ๏ธ Generate ERD Diagram
        entry: erdify ./src/database --title "Database Schema" -o docs/erd.puml
        language: system
        files: ^src/database/.*\.py$
        pass_filenames: false

Or using uvx (no installation required):

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: generate-erd
        name: ๐Ÿ—ƒ๏ธ Generate ERD Diagram
        entry: uvx erdify ./src/database --title "Database Schema" -o docs/erd.puml
        language: system
        files: ^src/database/.*\.py$
        pass_filenames: false

Setup:

# Install pre-commit
pip install pre-commit

# Install the hooks
pre-commit install

# Run manually on all files
pre-commit run generate-erd --all-files

How it works:

  • ๐Ÿ” Only triggers when files in src/database/ change
  • ๐Ÿ“ Automatically regenerates docs/erd.puml
  • โœ… Stages the updated diagram with your commit
  • ๐Ÿšซ Fails if the diagram would change (ensuring docs stay in sync)

Tip: Add docs/erd.puml to your staged files before committing, or use the --all-files flag to regenerate.

๐Ÿ“‹ Supported Features

Feature Status Notes
Primary Keys โœ… Field(primary_key=True)
Foreign Keys โœ… Field(foreign_key="table.column")
Nullable Fields โœ… str | None or Optional[str]
Default Values โœ… Field(default=value)
Indexes โœ… Field(index=True)
Enums โœ… Python Enum classes
Relationships โœ… Relationship()
Inheritance โœ… Mixin classes supported
Link Tables โœ… Many-to-many detection
Custom Table Names โœ… __tablename__ attribute
Exclude Patterns โœ… --exclude glob on class/table name
Key Inference โœ… --infer-keys for Pydantic/dataclass (id, <x>_id)
SQLModel โœ… Field() / Relationship()
SQLAlchemy 2.0 โœ… Mapped[...] / mapped_column()
Pydantic โœ… BaseModel subclasses, nested refs as relationships
Dataclass โœ… @dataclass, nested refs as relationships

๐Ÿ—บ๏ธ Roadmap

Recently shipped:

Feature Status Description
Exclude Option โœ… Done Exclude tables or entities from ERD generation using glob patterns
SQLAlchemy Support โœ… Done Native support for SQLAlchemy 2.0 (Mapped / mapped_column) models
Pydantic Support โœ… Done Generate ERDs from Pydantic models, with optional --infer-keys
Dataclass Support โœ… Done Support for standard Python dataclasses with type annotations

Have a feature request? Please open an issue on GitHub to discuss it!

๐Ÿค Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

๐Ÿ”’ Security

For security concerns, please see SECURITY.md.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

erdify stands on the shoulders of great open-source projects:

Supported model frameworks

  • SQLModel - The awesome SQL database library
  • SQLAlchemy - The Python SQL toolkit and ORM
  • Pydantic - Data validation using Python type hints
  • dataclasses - Python standard-library data classes

Rendering & output

  • PlantUML - For the diagram rendering
  • Graphviz - Layout engine behind PlantUML's ER diagrams

Tooling & infrastructure

  • uv - Packaging, builds and dependency management
  • Ruff - Linting and formatting
  • mypy - Static type checking
  • pytest - Testing framework
  • pre-commit - Git hook management

Community


Made with โค๏ธ by Devsuit GmbH

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

erdify-0.3.1.tar.gz (17.1 kB view details)

Uploaded Source

Built Distribution

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

erdify-0.3.1-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file erdify-0.3.1.tar.gz.

File metadata

  • Download URL: erdify-0.3.1.tar.gz
  • Upload date:
  • Size: 17.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for erdify-0.3.1.tar.gz
Algorithm Hash digest
SHA256 afa240696924af27c5c27d24a05a09df8ad9979093b9cb48fd2caa596b678ee7
MD5 01cc18e17d1b25720a1055288b7224af
BLAKE2b-256 f1e05ccaf701b93d4ff4b91d55a89b86d1098f91b7134168a24b27b70ef6ca1c

See more details on using hashes here.

File details

Details for the file erdify-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: erdify-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for erdify-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4da622cb48b2f7d919a4cbf7e4154db2168ff38812a719b7d8dd41145ee0e61e
MD5 a68dad3fc41f197f03a949735c56e388
BLAKE2b-256 f4bdfe59a87270cba56a322179f6e24abb1a0bd78d4293950c3f81986b700b5c

See more details on using hashes here.

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