Skip to main content

Generate PlantUML ERD diagrams from SQLModel, SQLAlchemy, Django, 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, Django, 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, Django ORM, Pydantic and standard-library dataclasses. No database connection required!

โœจ Features

  • ๐Ÿ“Š Automatic ERD Generation - Parse your models and generate PlantUML diagrams
  • ๐Ÿงฌ 5 Frameworks - SQLModel, SQLAlchemy 2.0 (Mapped[...]/mapped_column()), Django ORM, 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] [--django-raw-types]
                    [--no-enums] [--no-relationships] [-v]
                    input

Generate PlantUML ERD diagrams from SQLModel, SQLAlchemy, Django, 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, django, 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'
  --django-raw-types    For Django models, show original field names (CharField,
                        TextField) instead of mapped Python types (str, int)
  --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, django, 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, Five Frameworks

erdify supports five 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, SQLAlchemy and Django versions declare keys explicitly (Django via its implicit id and ForeignKey). 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
Django ORM
from django.db import models


class User(models.Model):       # implicit `id` PK; CharField/EmailField -> str
    name = models.CharField(max_length=100)
    email = models.EmailField()

    class Meta:
        db_table = "user"


class Order(models.Model):
    user = models.ForeignKey(    # -> user_id : int foreign key column
        User, on_delete=models.CASCADE)
    total = models.FloatField()  # -> float

    class Meta:
        db_table = "order"

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)
Django ORM models.Model subclass primary_key=True or implicit id, ForeignKey/OneToOneField ForeignKey (N:1), OneToOneField (1:1), ManyToManyField (M:N, incl. through=)
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__, or a Django class Meta: abstract = True base) 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.

Django ORM

erdify parses Django models from source โ€” no Django runtime, settings, or app registry required. A models.Model subclass becomes an entity; abstract bases (class Meta: abstract = True) are inherited but not drawn, and a class Meta: db_table = "..." overrides the table name.

from django.db import models


class Author(models.Model):          # implicit `id` primary key (int)
    name = models.CharField(max_length=100)


class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)   # N:1
    tags = models.ManyToManyField("Tag")                            # M:N
    profile = models.OneToOneField("Profile", on_delete=models.CASCADE)  # 1:1

    class Meta:
        db_table = "catalog_book"

Relationship targets are resolved by class name, including "self" and "app.Model" string references. A ManyToManyField(through=LinkModel) is drawn through the link model's own foreign keys (no spurious direct edge), exactly like SQLAlchemy secondary=.

By default Django field types are mapped to readable Python types (CharField โ†’ str, IntegerField/AutoField โ†’ int, DateTimeField โ†’ datetime, โ€ฆ) so mixed-source diagrams stay consistent. Ambiguous or unknown fields (JSONField, FileField, custom/third-party fields) keep their Django name rather than fake a type. Pass --django-raw-types to show the original Django field names everywhere instead.

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.5.0rc1.tar.gz (23.0 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.5.0rc1-py3-none-any.whl (25.3 kB view details)

Uploaded Python 3

File details

Details for the file erdify-0.5.0rc1.tar.gz.

File metadata

  • Download URL: erdify-0.5.0rc1.tar.gz
  • Upload date:
  • Size: 23.0 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.5.0rc1.tar.gz
Algorithm Hash digest
SHA256 b485237d7c4bc6f8c31494ce75eb0abfe425c40c0b5125d9753d96b24db569f6
MD5 7a12e8560d83771970fff13d93f57b35
BLAKE2b-256 f4e9d7f005a544d082725b5c09f3f87407132082c38fe61fe595be8c8eb83037

See more details on using hashes here.

File details

Details for the file erdify-0.5.0rc1-py3-none-any.whl.

File metadata

  • Download URL: erdify-0.5.0rc1-py3-none-any.whl
  • Upload date:
  • Size: 25.3 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.5.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 7e13ae06fb002e6da2b22b161e5dc1c7fd4a66dd1c2b9225a2d1871ef0a21748
MD5 b401b59ff5dcf9889430c856df6517ca
BLAKE2b-256 ac7bd785b5a417bac9a62dd61d666c6ab7d8af02db772f815285947f343f8ca7

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