Skip to content

Support django-ninja schemas: recognise Pydantic bases defined outside the scanned files #171

Description

@FabianClemenz

Problem

erdify does not recognise schemas from the django-ninja ecosystem. Reproduced on v0.13.0:

# models.py
from ninja import Schema

class AuthorOut(Schema):
    id: int
    name: str
$ erdify ./app --infer-keys
Error: No tables found in ./app
  Scanned 1 .py/.sql file(s); 1 matched --include 'models.py'.
  Files matched but held no recognized models.

Replace Schema with BaseModel in the same file and it is detected immediately.

Cause

_classify_source() treats a class as Pydantic only when BaseModel appears in its bases, directly or transitively — and "transitively" is resolved only across the scanned files (parser.py, _is_pydantic_model). ninja.Schema does subclass pydantic.BaseModel, but that inheritance lives inside the installed ninja package, which erdify never reads. The base is therefore unresolvable and the class is skipped.

This is the documented "only scanned files exist" limitation applied to classification rather than to relationship targets, and it is not specific to django-ninja. It hits any Pydantic-derived base defined outside the scan:

  • ninja.Schema (django-ninja)
  • a shared BaseSchema in an internal company library
  • SQLModel subclasses re-exported from another package

Two separate problems

These have very different costs and should not be conflated.

1. Base-class recognition — small, and worth doing

For schemas with explicit field annotations, everything erdify needs is already in the class body; only the classification gate is wrong.

Proposal: --base-classes NAME [NAME ...] and [tool.erdify] base_classes, naming extra base classes to treat as Pydantic models.

erdify ./api --base-classes Schema --infer-keys
[tool.erdify]
base_classes = ["Schema", "BaseSchema"]

Config-driven rather than a hardcoded list of known third-party names: the internal-library case is at least as common as the django-ninja one, and a hardcoded list can never solve it. A small curated default set could be layered on later if it proves worth it.

This alone covers:

  • django-ninja Schema subclasses with explicit annotations
  • django-ninja-extra — it turns out this needs nothing of its own. It adds controllers, permissions and dependency injection (@api_controller, ControllerBase), and reuses ninja.Schema/Pydantic for schemas. Fixing (1) covers it entirely; no separate work.

2. Model-derived schemas — much larger, probably a separate issue

Both ninja.ModelSchema and ninja_schema.ModelSchema derive their fields from a Django model through an inner class, with no annotations in the class body at all:

# django-ninja — inner class is Meta
from ninja import ModelSchema

class UserSchema(ModelSchema):
    class Meta:
        model = User
        fields = ["id", "username", "first_name"]
        # also: exclude, fields_optional
# ninja-schema — inner class is Config
from ninja_schema import ModelSchema

class UserSchema(ModelSchema):
    class Config:
        model = UserModel
        include = ["id", "first_name", "email"]
        # also: exclude, optional, depth

Recognising the base class here buys nothing — erdify would find an entity with zero fields, which is worse than skipping it. Supporting these properly means:

  • resolving Meta.model / Config.model to a Django model whose source is also in the scan (otherwise there is nothing to read, and this fails the same way for a model imported from another app that --include misses),
  • reusing the existing Django field extraction for that model,
  • applying fields / include / exclude / fields_optional / optional,
  • deciding what the result even means in an ERD. A response schema is a projection of a table, not a table. Rendering it as a second entity next to the model it mirrors is arguably noise — it may belong behind --sources ninja or be excluded by default.
  • depth (ninja-schema) pulls in nested related schemas, which compounds all of the above.

Note the two libraries disagree on both the inner class name (Meta vs Config) and the field-selection keyword (fields vs include), so this is two mappings, not one.

Suggestion: land (1) on its own; open a separate issue for (2) once there is a clear answer to the "is a response schema an entity?" question. Interest from django-ninja users would be good evidence either way.

Why this is worth doing

django-ninja is one of the faster-growing Django API frameworks, its users have exactly the problem erdify solves, and no existing ERD tool covers them — django-extensions graph_models sees the Django models but not the schemas, and importing-based tools need the full app loaded.

Scope for (1)

  • --base-classes flag and [tool.erdify] base_classes key, threaded through ASTDatabaseParser to _is_pydantic_model
  • Test fixture with ninja.Schema-style subclasses (a local stand-in base — no runtime dependency on django-ninja, consistent with the existing fixtures)
  • Cover the internal-shared-base case too, not just the django-ninja one
  • docs/usage/filtering.md or usage/cli.md, plus regenerate the CLI block
  • docs/frameworks/limitations.md — update the base-class note to point at the new escape hatch
  • CHANGELOG.md under Added

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions