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
  • ๐ŸŽš๏ธ Source Filtering - Restrict the diagram to specific model kinds with --sources (e.g. ORM tables only)
  • ๐Ÿ“ฆ 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 association tables structurally (two FK columns that form the primary key), regardless of class name, including SQLAlchemy Core Table() association tables referenced via relationship(secondary=...)
  • ๐ŸŽจ 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 ...]]
                    [--sources [KIND ...]] [--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
  --sources [KIND ...]  Restrict which model kinds become entities. Choices:
                        sqlmodel, sqlalchemy, dataclass, pydantic. Default: all,
                        e.g. --sources sqlmodel sqlalchemy for DB tables only
  --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.

Filtering by Model Kind

Use --sources to restrict the diagram to specific model frameworks. By default all recognized kinds are drawn (sqlmodel, sqlalchemy, dataclass, pydantic). This is the precise alternative to --exclude when you want a pure DB-table ERD and don't want Pydantic DTOs or @dataclass query wrappers to leak in.

# Only real DB tables โ€” drops Pydantic/dataclass models entirely
erdify ./src/database --sources sqlmodel sqlalchemy

# ORM tables plus your Pydantic schemas, but no dataclasses
erdify ./src/database --sources sqlmodel pydantic

๐Ÿ’ก --sources filters by kind; --exclude filters by name. Combine them freely.

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.4.2.tar.gz (19.4 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.4.2-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: erdify-0.4.2.tar.gz
  • Upload date:
  • Size: 19.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • 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.4.2.tar.gz
Algorithm Hash digest
SHA256 6e75e89aaddf26a3c5c44f7974ca7421eb58c78e1cfcfb50da7988b7f913c622
MD5 602a7e657459e52e7f895269a839ccd7
BLAKE2b-256 90cc7a869c3bc0e4c0ce78cec7db84eeb3b9cd60dcda58e0c25342f97c1bf23b

See more details on using hashes here.

File details

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

File metadata

  • Download URL: erdify-0.4.2-py3-none-any.whl
  • Upload date:
  • Size: 21.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • 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.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 efe422e1a5456ae7235095f84ed076f97340e43d263d9f2befeafe989590ce17
MD5 cd8f53c6b76f40d38d7bdc1931f6a100
BLAKE2b-256 0868f18e5f96ff6a33f52fdbe25bde57746a7219f414a9d85355670ce5a8cf41

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