2025, Sep 23 03:00

Fixing Beanie Link errors in FastAPI: stop instantiating Link, add the document and let Beanie save a DBRef

Learn why manual Link(...) in Beanie causes DBRef and document_class errors in FastAPI, and how to correctly reference documents by adding the document.

Beanie links in FastAPI: why manual Link(...) fails and what to do instead

When wiring related documents in Beanie, it’s tempting to create Link objects by hand. The result is a cascade of confusing type errors. The fix is much simpler than it looks: add the document itself, not a Link wrapper. Below is a minimal example that reproduces the issue and shows the correct way to establish the relation.

Problem setup: Beanie Documents and a FastAPI route

The models define two Beanie documents. One references the other via a set of links. The API endpoint tries to attach a referenced item.

# models.py
from beanie import Document, Link
class Alpha(Document):
    prime: int
    note: str
class Beta(Document):
    score: float
    alpha_refs: set[Link[Alpha]] = set()
# main.py
from fastapi import FastAPI, HTTPException, status
from beanie import PydanticObjectId, Link
from .models import Alpha, Beta
app = FastAPI()
@app.post("/beta/{beta_id}/attach/{alpha_id}")
async def attach_alpha(beta_id: PydanticObjectId, alpha_id: PydanticObjectId):
    beta_obj = await Beta.get(beta_id)
    if not beta_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    alpha_obj = await Alpha.get(alpha_id)
    if not alpha_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    # attempt that triggers errors
    beta_obj.alpha_refs.add(Link(alpha_obj))
    await beta_obj.save()
    return beta_obj

Trying to manually construct a Link results in errors. The first attempt complains about a missing parameter.

Parameter 'document_class' unfilled

Filling the parameter leads to another type error.

Expected type 'DBRef', got 'Alpha' instead

Switching to pass the id still doesn’t align with the expected type.

Expected type 'DBRef', got 'PydanticObjectId | None' instead

What’s actually going on

The stumbling block is the manual creation of Link objects. Although the field is annotated as set[Link[Alpha]], you are not supposed to instantiate Link yourself. Beanie takes the plain document you add and, on save, transforms it into the proper DBRef behind the scenes. Supplying your own Link breaks this contract and produces the type and parameter mismatches shown above.

The fix: let Beanie handle the link conversion

Add the document object to the link field and save. Beanie will convert it into a DBRef on persistence.

# main.py (fixed core line)
from fastapi import FastAPI, HTTPException, status
from beanie import PydanticObjectId
from .models import Alpha, Beta
app = FastAPI()
@app.post("/beta/{beta_id}/attach/{alpha_id}")
async def attach_alpha(beta_id: PydanticObjectId, alpha_id: PydanticObjectId):
    beta_obj = await Beta.get(beta_id)
    if not beta_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    alpha_obj = await Alpha.get(alpha_id)
    if not alpha_obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    beta_obj.alpha_refs.add(alpha_obj)
    await beta_obj.save()
    return beta_obj

This minimal change avoids all intermediate type issues because the ODM performs the correct translation during save.

Why this matters

Understanding how Beanie treats references keeps your data layer predictable and your API handlers concise. Misusing Link leads to cryptic errors about DBRef and document_class that distract from the actual goal. By relying on Beanie’s built-in conversion, you keep the model’s intent clear and reduce surface area for type mismatches.

Takeaways

When relating Beanie documents in FastAPI endpoints, don’t construct Link by hand. Add the document instance to the link field and save the parent document. This aligns with Beanie’s design, avoids DBRef and parameter errors, and keeps your code straightforward.

The article is based on a question from StackOverflow by Ambitions and an answer by Pokemon.