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.
Bug: duplicate
@sc.typeregistrations with the same GraphQL name leak fields acrossoverride=TrueregistrationsRepository: https://github.com/gazorby/strawchemy
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 itsinclude=[...]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=Truesuggests the later registration should win, but stale fields from the previous definition survive.Minimal repro
Expected
ParentSlimshould reflect its own registration config:Actual
ParentSliminherits the earlier wider field set:Building a schema from both fields also exposes the wider
ParentNodeshape for both resolvers:Verified against
54392fd84a1e074f59f04b1355aba850b8f43433mainat time of filing:580eac94eafa68887a75c76fa5c84fde6a981903713a5d51658486ff06e41f3fbe1b6035a71b710aWorkaround in use today
We wrap
@sc.typeand, after decoration, prune the finalizedStrawberryObjectDefinition.fieldsback to exactly:That prevents stale fields from a previous registration of the same model/type name from leaking into the later registration.
Suggested fix
When
override=Trueis used for an existing GraphQL type name, the replacement type definition should be rebuilt from the current registration'sDTOConfig/ class body only. In particular, relation fields and aggregate fields from a previous registration should not survive unless the new registration includes them.