Return Typed Output Objects Through GraphQL¶
Use GraphQLType when a GraphQL response needs a nested object shape but the value does not need a manager identity, lifecycle, or independent operation. This recipe defines a frozen output value, returns it from a @graph_ql_property, and queries the generated nested object.
Define the output value¶
Concrete GraphQLType subclasses become frozen dataclass-style values. Their annotated fields become GraphQL fields; field(default_factory=...) supplies a fresh default for each Python instance.
from __future__ import annotations
from dataclasses import field
from general_manager import GraphQLType
class ProjectHour(GraphQLType):
task_id: int = field(metadata={"is_identifier": True})
hours: float
note: str | None = None
tags: list[str] = field(default_factory=list)
The generated GraphQL object is ProjectHourType. Required annotations become non-null fields, T | None becomes nullable, and collection element nullability follows the element annotation. GraphQLType declarations do not create root queries, mutations, subscriptions, filters, or input objects. The explicit field marker makes taskId: ID!; hours remains Float! and tags remain strings. The native Python task key stays an integer. Field names do not imply identity, and absent/false metadata keeps ordinary scalar types.
Import the declaration module during application startup, before GeneralManager builds the schema:
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self) -> None:
from . import graphql_types # noqa: F401
from . import managers # noqa: F401
Return the value from a GraphQL property¶
The output type is available only through a field that references it. A calculation manager is useful here because its generated detail field accepts the declared input and exposes the property result:
from __future__ import annotations
from general_manager import GeneralManager, graph_ql_property
from general_manager.interface import CalculationInterface
from general_manager.manager import Input
from .graphql_types import ProjectHour
class ProjectHoursSummary(GeneralManager):
class Interface(CalculationInterface):
project_id = Input(int, possible_values=(42,))
@graph_ql_property(cache="none")
def hours(self) -> list[ProjectHour]:
return [
ProjectHour(
task_id=9007199254740993,
hours=8.5,
note="Ready for review",
tags=["billable", "forecast"],
)
]
The owning manager's property is the authorization boundary. Nested manager fields keep their manager-level read checks, but scalar and nested GraphQLType fields do not receive an independent permission check. Omit or pre-authorize sensitive values before constructing the output object.
Query the generated object¶
The calculation manager's generated detail field uses camel-case GraphQL names. The output object can be selected like any other GraphQL object:
query ProjectHours {
projectHoursSummary(projectId: 42) {
hours {
taskId
hours
note
tags
}
}
}
The response has the nested shape declared by ProjectHour:
{
"data": {
"projectHoursSummary": {
"hours": [
{
"taskId": "9007199254740993",
"hours": 8.5,
"note": "Ready for review",
"tags": ["billable", "forecast"]
}
]
}
}
}
The output mapper accepts scalar annotations, Measurement, registered managers, registered output types, and list[T], tuple[T, ...], or set[T]. Bare collections, fixed-length tuples, Any, Annotated[...], unresolved references, and unions with more than one non-null member fail schema generation with a field-specific error. The output-type contract is available from GeneralManager 0.75.0.
For additional flat identity shapes, use the same field metadata with native str or UUID, list[T], set[T], or tuple[T, ...]. For example, related_ids: list[int] = field(metadata={"is_identifier": True}) becomes [ID!]!, while related_ids: list[int | None] | None with that marker becomes [ID]. Optional UUID identity fields become nullable ID. Defaults and default_factory remain standard dataclass behavior. Unmarked UUIDs remain outside the ordinary scalar allowlist. Invalid flag values and unsupported identity annotations raise a field-specific TypeError.
Opting in changes the GraphQL scalar and integer JSON values to strings; regenerate client types and preserve IDs as opaque strings, including leading zeros. It does not change Python/DB values or add output-object filters.