Skip to content

Bug: duplicate @sc.type registrations with the same GraphQL name leak fields across override=True registrations #171

Description

@adiberk

Bug: duplicate @sc.type registrations with the same GraphQL name leak fields across override=True registrations

Repository: https://github.com/gazorby/strawchemy

Disclosure: Found during a large Flask + Graphene 2 → FastAPI + Strawberry/strawchemy migration with AI assistance. Repro below runs in isolation.

Summary

When the same SQLAlchemy model is registered more than once with the same GraphQL type name and override=True, the later registration can inherit fields from the earlier registration, even when its include=[...] list is narrower.

This only reproduced for me when the registrations share the same GraphQL name. If the two decorated classes produce different GraphQL names, each definition keeps its own field set.

This matters for larger schemas where a migration may temporarily register the same model under a canonical node name from more than one module/slice, or where an explicit user registration is meant to replace an earlier auto/stub registration. override=True suggests the later registration should win, but stale fields from the previous definition survive.

Minimal repro

from __future__ import annotations

import strawberry
from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from strawchemy import Strawchemy, StrawchemyConfig


class Base(DeclarativeBase):
    pass


class Parent(Base):
    __tablename__ = "parent"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    children: Mapped[list[Child]] = relationship(back_populates="parent")


class Child(Base):
    __tablename__ = "child"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
    parent: Mapped[Parent] = relationship(back_populates="children")


sc = Strawchemy(StrawchemyConfig(dialect="postgresql"))


@sc.type(Child, name="ChildNode", include=["id"], override=True)
class ChildNode:
    pass


@sc.type(Parent, name="ParentNode", include=["id", "name", "children"], override=True)
class ParentWide:
    pass


@sc.type(Parent, name="ParentNode", include=["id"], override=True)
class ParentSlim:
    pass


print("wide", [f.name for f in ParentWide.__strawberry_definition__.fields])
print("slim", [f.name for f in ParentSlim.__strawberry_definition__.fields])

Expected

ParentSlim should reflect its own registration config:

wide ['children_aggregate', 'children', 'id', 'name']
slim ['id']

Actual

ParentSlim inherits the earlier wider field set:

wide ['children_aggregate', 'children', 'id', 'name']
slim ['children_aggregate', 'children', 'id', 'name']

Building a schema from both fields also exposes the wider ParentNode shape for both resolvers:

type ParentNode {
  childrenAggregate: ChildAggregate!
  children: [ChildType!]!
  id: Int!
  name: String!
}

Verified against

Workaround in use today

We wrap @sc.type and, after decoration, prune the finalized StrawberryObjectDefinition.fields back to exactly:

include_list ∪ class_body_fields

That prevents stale fields from a previous registration of the same model/type name from leaking into the later registration.

Suggested fix

When override=True is used for an existing GraphQL type name, the replacement type definition should be rebuilt from the current registration's DTOConfig / class body only. In particular, relation fields and aggregate fields from a previous registration should not survive unless the new registration includes them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions