Skip to content

Types

SelectorKind

SelectorKind

Bases: str, Enum

Whether a :class:SelectorSpec returns many objects or a single one.

The kind is what tells the framework — and any future caller that reuses a spec outside an HTTP request — whether to materialize the selector's return as a collection (LIST) or as a single instance with retrieve-flavoured 404 semantics (RETRIEVE). Mounting a spec on a mismatched view (e.g. a LIST spec on a :class:SelectorRetrieveView) raises :exc:~django.core.exceptions.ImproperlyConfigured at as_view() time.

Inheriting from str keeps the value JSON-serializable and print-friendly while still behaving as a proper enum for is / == checks.

SelectorSpec

SelectorSpec dataclass

Bases: Generic[ResultT, ExtraT]

All wiring for a single read action in one record.

Used as a value in action_specs on viewsets, as the spec= argument to :class:SelectorListView / :class:SelectorRetrieveView, and as the output_selector_spec field on :class:ServiceSpec (where it describes the post-mutation re-fetch).

Generic parameters (both default to Any):

  • ResultT — the selector's return type.
  • ExtraT — a TypedDict describing the keys returned by kwargs.

All fields are keyword-only: SelectorSpec(kind=SelectorKind.LIST, selector=fn) rather than positional. kind is required and has no default — see below.

Fields:

  • kind — required :class:SelectorKind discriminator (LIST vs RETRIEVE). Drives the dispatcher: RETRIEVE materializes a QuerySet via .first() and raises :exc:~rest_framework.exceptions.NotFound on None / missing-object, LIST returns whatever the selector returns unchanged. Also drives the fail-fast check that the spec is mounted on a compatible view (a LIST spec on :class:SelectorRetrieveView raises at as_view()). Making it explicit lets a spec be reused outside a request — from a management command, a cron job, or any non-DRF caller — without the semantics living implicitly in the call site.
  • selector — callable invoked by get_queryset() (list) or get_object() (retrieve). None means "use the configured queryset / default DRF behaviour".
  • allow_noneRETRIEVE-only knob for the None / missing-object case. False (the default) keeps the standard behaviour: raise :exc:~rest_framework.exceptions.NotFound. True expresses a nullable-resource contract: the standalone retrieve view and the retrieve viewset mixin render 200 with a JSON null body, skipping the output serializer. The flag is ignored when the spec is nested — :attr:ServiceSpec.output_selector_spec keeps its authoritative-None → 204 contract, and :attr:ServiceSpec.instance_selector_spec always 404s (an update against a missing row is not a nullable read).
  • output_serializer — DRF Serializer subclass used by get_serializer_class() for this action. None falls back to DRF's standard serializer_class.
  • kwargs — callable returning extra kwargs to merge into the pool the selector receives. Co-locating it with the spec lets each action declare its own contract — no if self.action == ... branching in a catch-all get_selector_kwargs.
  • permission_classes — overrides the calling view's permission_classes for the action the spec backs. None (the default) means "inherit the view's class-level permissions"; an empty sequence means "no permissions" explicitly. Forwarded through DRF's @action(permission_classes=...) for the @selector_action decorator, and surfaced via get_permissions for the viewset mixins and standalone views. Ignored when the spec is nested under :attr:ServiceSpec.output_selector_spec — the surrounding mutation action's permissions apply.
  • output_serializer_context — per-spec hook for the response serializer's context= dict. Sits at the most-specific layer of the resolution chain (view.get_serializer_contextview.get_output_serializer_contextview.get_<action>_output_serializer_context → spec hook), so it wins on overlapping keys. None (the default) leaves the three earlier layers intact. Selectors don't validate input, so there's no symmetrical input_serializer_context.

The provider is invoked through the framework's keyword-pool convention, so it declares only what it needs: view, request, and/or the resolved data being serialized — page on a LIST spec (the paginated object list, or the full queryset when pagination is off) or instance on a RETRIEVE spec — or **kwargs for the whole pool. Any subset works (just view, just request, just page, none). This lets the provider run a single batched query against the exact objects being serialized and propagate the result through context — e.g. lambda *, page: {"votes": tally(page)} or lambda view, request, *, page: {"votes": tally(page)}. The hook always runs after the data is resolved. - select_related / prefetch_related / annotations — declarative queryset shaping applied to the selector's return value before it leaves :func:dispatch_selector_for_spec. select_related is a sequence of relation names (forwarded as qs.select_related(*spec.select_related)); prefetch_related is a sequence of relation names or :class:Prefetch objects; annotations is a mapping merged into a single .annotate(**...) call. Use these for the common case where the same shaping applies every request — they're introspectable for OpenAPI / future tooling. - extend_queryset — dynamic escape hatch. A Callable[[QuerySet, ServiceView, Request], QuerySet] invoked after the declarative fields have applied, so it always sees the fully statically-shaped queryset. Use it when the shaping depends on the request (e.g. only prefetch when a query string opts in). Synchronous only — it manipulates the queryset's lazy expression tree, not the database. - filter_set — transport-neutral filtering applied to the selector's QuerySet. Holds a django-filter FilterSet class (or any object honouring the same (data, queryset) -> .qs contract): the dispatcher calls filter_set(data=request.query_params, queryset=qs).qs, so the declaration of which fields are filterable (with which lookups) lives on the spec while the values come from the request. Applied after the four shaping fields above and before the retrieve .first() materialization, so it composes with shaping and narrows both LIST and RETRIEVE selectors — closing the retrieve-path gap where RetrieveModelMixin runs no filter step.

Because the values are read off request.query_params, a FilterSet here is exactly what :class:~django_filters.rest_framework.DjangoFilterBackend applies view-side, so on the list path it replaces that backend rather than stacking with it — wiring both for one action raises at as_view(). Replacing it means keeping its contract, so invalid filter input is rejected with a 400 rather than silently ignored: the FilterSet is validated via is_valid() and its errors are raised as a DRF :exc:~rest_framework.exceptions.ValidationError. (Reading .qs without validating would return the unfiltered queryset in django-filter's default non-strict mode — a bad ?field= value would answer 200 with unfiltered rows.) Only enforced when the duck-typed object actually exposes is_valid; a bare (data, queryset) -> .qs stand-in that doesn't opt into validation keeps its pass-through behaviour. The dispatcher also forwards the request into the FilterSet when its constructor declares one (as django-filter's does), so a request-scoped FilterSet — self.request.user scoping, a request-aware ModelChoiceFilter queryset — sees the same self.request it would behind DjangoFilterBackend rather than None. That request is real on the HTTP / MCP paths and a synthetic off-HTTP one whose user and query_params are faithful (headers / session are best-effort there); a bare (data, queryset) stand-in never receives it. None (the default) applies no filtering. Reach for filter_set only when the selector returns a QuerySet; when it returns an aggregate / computed object the ?param values are computation inputs — use kwargs / get_selector_kwargs() instead.

All five shaping fields (select_related / prefetch_related / annotations / extend_queryset / filter_set) require selector to be set and the selector to return a Django :class:QuerySet. Configuring any of them with no selector raises :exc:ImproperlyConfigured at as_view() time; a non-QuerySet return raises at request time.

ServiceSpec

ServiceSpec dataclass

Bases: Generic[InputT, ResultT, ExtraT]

All wiring for a single mutation action in one record.

Used as a value in ServiceViewSet.action_specs and as the spec= argument to :func:service_action / :class:ServiceCreateView / :class:ServiceUpdateView / :class:ServiceDeleteView.

Generic parameters are optional and purely informational for type checkers:

  • InputT — the validated-data type produced by input_serializer. For dataclass-based serializers this is the dataclass; for plain ModelSerializer it is typically dict[str, Any].
  • ResultT — the value returned by the service callable, and (when output_selector_spec is set) the input to its selector.
  • ExtraT — a TypedDict describing the keys returned by kwargs.

All three default to Any, so ServiceSpec(service=fn) keeps working unchanged.

Fields are grouped by what they configure: the service callable itself, the input pipeline (input_*), the output pipeline (a single nested :class:SelectorSpec), and the cross-cutting concerns (kwargs, permission_classes).

success_status is left as None so each consumer can supply its own action-appropriate default (201 for create, 200 for update, 204 for destroy). It may also be a callable resolved through the framework keyword pool — declaring any subset of result / instance / request / view (or **kwargs) — returning the status int. This covers upserts whose code depends on the outcome (200 for an existing row, 201 for a freshly created one); the callable sees the service's return value as result. A None return from the field itself is not meaningful — return an int. OpenAPI can't resolve a callable statically, so the generated schema documents the mixin default for the dynamic case.

input_data is the symmetrical hook for the serializer's input. Returns a mapping merged on top of request.data before the input_serializer validates it — useful for lifting URL kwargs (e.g. parent IDs from nested routes) into fields the serializer can cross-validate. Server-provided keys win on conflict. The provider is invoked through the framework's keyword pool, so it declares any subset of view / request plus instance (the resolved mutation target, None on create) — or **kwargs — receiving only what it names, so pre-validation input mutation that depends on the current row has a home. The same declare-to-receive rule applies to the get_input_data / get_<action>_input_data view hooks.

input_serializer_context is a per-spec hook for the input serializer's context= dict. It sits at the most-specific layer of the resolution chain (view.get_serializer_contextview.get_input_serializer_contextview.get_<action>_input_serializer_context → spec hook), so the spec wins on overlapping keys. None (the default) leaves the three earlier layers intact. The symmetrical output hook lives on the nested output_selector_spec.output_serializer_context; that hook may additionally declare a result keyword to receive the final (post-selector) instance being serialized — passed only when declared — so it can run a single batched query against it and propagate the outcome through context. The output hook always runs after the service and output selector have resolved result.

output_selector_spec is the full output pipeline collapsed into a single :class:SelectorSpec. Its kind declares the response cardinality: :attr:SelectorKind.RETRIEVE (the default) re-fetches a single instance (typical pattern: the service returns a freshly created/updated instance, the output_selector_spec.selector re-fetches it with the relations the response serializer needs, and the spec's output_serializer renders the result); :attr:SelectorKind.LIST re-fetches and renders a set (many=True) and is valid only alongside collection_selector_spec — the bulk-output twin that lets a collection mutation return the affected set. None (the default) means "render the service's return value directly". The nested spec's permission_classes and kwargs are ignored — the surrounding mutation's permissions and kwargs chain apply.

instance_selector_spec is the input-side twin of output_selector_spec: a nested :class:SelectorSpec (kind must be :attr:SelectorKind.RETRIEVE) that resolves the instance an update / destroy / detail action targets, embedding the lookup in the spec instead of relying on the view's queryset / get_object() chain. The selector's kwarg pool is {request, user} plus the URL kwargs (plus the standard selector extras chain), so selector=lambda *, pk: Project.objects.filter(pk=pk) resolves the row from the route. Resolution happens before input validation — the resolved instance is handed to the input serializer (DRF-style serializer(instance, data=..., partial=...)) and seeded into the service kwarg pool as instance. A None / missing resolution raises :exc:~rest_framework.exceptions.NotFound (the nested spec's allow_none flag is ignored — an update against a missing row is always a 404), and object-level permissions (check_object_permissions) run against the resolved instance. The queryset-shaping fields apply; permission_classes, output_serializer, and output_serializer_context on the nested spec are ignored. None (the default) keeps today's get_object() chain.

partial overrides the transport-derived partial-validation flag. None (the default) inherits the flag the calling surface derives (False for PUT/POST, True for PATCH); True / False forces it regardless of HTTP method — e.g. partial=False on an action_specs["partial_update"] entry makes a PATCH endpoint enforce required fields like a PUT. Applied once, at dispatch_mutation_for_spec, so it is honoured uniformly by the viewset mixins, the standalone views, and @service_action.

document_service_error is an OpenAPI-only flag controlling whether the generated schema documents the 422 :class:ServiceError response (it has no effect on runtime behaviour — a service may always raise ServiceError). None (the default) gates the 422 on whether the operation validates input (input_serializer is not None), so a no-input mutation (e.g. a plain delete) doesn't carry a spurious 422 in its schema. Set True to document it anyway (a no-input service that does raise ServiceError) or False to drop it (an input-bearing service that never raises one). Only consulted when the [spectacular] extra is enabled via :func:~rest_framework_services.openapi.enable_openapi.

many and collection_selector_spec are the two bulk shapes, mutually exclusive with each other. many=True validates the request body as a list (input_serializer runs many=True) and renders the result list the same way — the service receives the validated list as data and loops itself (bulk_create / a comprehension), so one call does the batch. collection_selector_spec is the LIST-kind twin of instance_selector_spec: its resolved set (a queryset or any iterable, scoped by the selector + filter_set) is seeded into the pool as collection for the service to .delete() / .update() / iterate — an instance-less "operate on the filtered set" action (bulk delete/update), where an empty set is a harmless no-op. Its selector's kwarg pool carries the request's query params and body plus the view's URL kwargs, so a nested-route bulk (/parents/{parent_pk}/children/) can scope by parent_pk — matching instance_selector_spec. Route captures win over client query / body on a key conflict, so a filter value can't override the route scope. Both run all-or-nothing under atomic=True; authorization is per-set (the view / spec permission_classes plus the scoped selector), with no per-row check.

kwargs is a callable that returns extra kwargs to merge into the pool the service receives. Co-locating it with the spec lets each action declare its own contract — no if self.action == ... branching in a catch-all get_service_kwargs. See :class:ServiceView for the attributes available on the view argument.

permission_classes overrides the calling view's permission_classes for the action the spec backs. None (the default) means "inherit the view's class-level permissions"; an empty sequence means "no permissions" explicitly. Forwarded through DRF's @action(permission_classes=...) for the @service_action decorator, and surfaced via get_permissions for the viewset mixins and standalone views.

response_finalizer is a post-serialization hook for HTTP response side effects (cookies, headers, a swapped response). It runs on the 2xx path only, after the output serializer has produced the Response and before it is returned (pre-render); error paths bypass it. Invoked through the framework keyword pool, it declares any subset of response / result / request / view / instance / data (or **kwargs) and returns either a Response (which replaces the built one) or None (which keeps it) — so lambda *, response: response.set_cookie(...) or response attaches a cookie, and returning a fresh Response swaps it wholesale. Unlike the service/selector pool it does receive view (a documented exception — a response decision legitimately needs view/request context). result is the service's return value (the flags carrier), so the idiomatic pattern keeps services DRF-free: the service returns domain flags on its result DTO and the finalizer translates flags → transport effects. HTTP-only: it is skipped on the transport-neutral path (dispatch_spec / call_service / MCP), which builds no Response. On the bulk path instance / data are absent and result is the dispatched value (post-output-selector).

PolymorphicServiceSpec

PolymorphicServiceSpec dataclass

A single action that accepts several mutually exclusive payload shapes.

Each variant has its own input serializer and service, bundled as a full :class:ServiceSpec under a string key. A discriminator callable inspects the request and returns the key; dispatch then proceeds through the chosen spec exactly as a plain ServiceSpec would. Usable anywhere a ServiceSpec is: an action_specs entry and the spec= of @service_action.

PolymorphicServiceSpec(
    discriminator=resolve_flow,        # pool: {request, data, user, view} → key
    specs={"email": email_spec, "token": token_spec},
)

discriminator is resolved through the framework keyword pool — it declares any subset of request / data (the raw request.data) / user / view (or **kwargs) — and returns a key present in specs. For a no-match / rejected payload it should raise (e.g. :exc:~rest_framework_services.exceptions.service_validation_error.ServiceValidationError, which the view layer maps to a 400); returning a key absent from specs is a configuration error and raises :exc:~django.core.exceptions.ImproperlyConfigured.

permission_strategy decides how get_permissions behaves, since DRF runs permissions before the body is necessarily parsed:

  • "union" (the default) — require the union of every variant's permission_classes; the request must satisfy them all. No early body read, and the conservative/secure failure mode (a mis-declared discriminator branch can't widen access). For the common "same auth, different payload shapes" case this is identical to "require_identical".
  • "discriminate" — run the discriminator early (reading the raw body) and apply only the chosen variant's permission_classes. Most precise, for the genuine "each variant is a different privilege" case; accepts the early-body-read tradeoff.
  • "require_identical" — validated at as_view() time to require every variant to declare the same permission_classes; sidesteps ordering entirely.

The resolved concrete spec flows through the shared action→spec chain, so the chosen variant's serializer context, kwargs, and output pipeline all apply — the discriminator is resolved once per request and reused across dispatch, permissions, and serializer resolution.

ChangeResult

ChangeResult dataclass

Bases: Generic[ModelT]

Outcome of a mutation helper call.

instance is the model instance after the mutation. created is True iff this came from :func:create_from_input / :func:acreate_from_input. changes records every field whose value actually differed from its prior value (or from UNSET for creates). children carries one :class:ChildCollectionChange per reverse-FK collection written via the children= argument — empty for the common no-nested-write case.

The class is generic over the concrete model type: callers that pass Author into a mutation helper get back a ChangeResult[Author] whose .instance is typed as Author. The bare name ChangeResult (no parameter) resolves to ChangeResult[Model] and keeps working for callers that don't care.

changed_fields property

changed_fields: tuple[str, ...]

Names of every field present in :attr:changes.

get_field_change

get_field_change(field_name: str) -> FieldChange | None

Return the :class:FieldChange for field_name, or None.

get_child_change

get_child_change(relation: str) -> ChildCollectionChange | None

Return the :class:ChildCollectionChange for relation, or None.

FieldChange

FieldChange dataclass

One field's before/after pair from a mutation.

old will be UNSET for fields populated as part of a create (no prior value existed).

ChildSpec

ChildSpec dataclass

How to persist one reverse-FK ("one-to-many") child collection.

Passed in the children={relation_name: ChildSpec(...)} map of :func:~rest_framework_services.create_from_input / :func:~rest_framework_services.update_from_input (and their async siblings), and forwarded by the default :func:~rest_framework_services.create_model / :func:~rest_framework_services.update_model / :func:~rest_framework_services.delete_model services. The incoming child rows are read from data[relation_name]; each child is persisted by running it back through the same mutation helpers, so scalar / m2m / nested semantics compose recursively.

Fields:

  • model — the child model class.
  • fk — the name of the child's forward foreign-key field pointing at the parent (e.g. "author" for Book.author). It is set automatically on created children and used to resolve the parent's reverse manager.
  • match_key — the field used to pair an incoming row with an existing child (default "pk"). An incoming row whose match_key matches an existing child updates it; one with no match (or no key) is created. The same name is read off both the incoming mapping (item[match_key]) and the existing instance (getattr(child, match_key)), so serializers emitting "id" should set match_key="id".
  • mode"replace" (the default) matches incoming to existing, creates new, updates matched, and removes orphans (existing children absent from the incoming set); "merge" upserts only and never removes. An orphan is unlinked (its fk set to None) when the FK is nullable, else deleted — mirroring on_delete=SET_NULL vs CASCADE.
  • field_map / exclude_fields — forwarded to the per-child create_from_input / update_from_input call, exactly as for the parent.
  • m2m — optional callable (child_row) -> mapping deriving the child's many-to-many assignments from its incoming row (the per-child analogue of :func:~rest_framework_services.create_model's m2m).
  • children — a nested {relation_name: ChildSpec} map for grandchildren; recursion follows the declared tree, so depth is bounded by how deeply you nest specs.

The whole parent + children write runs inside the service's atomic block; field-level validation stays in the input serializer / dataclass — the helper owns persistence only.

ChildCollectionChange

ChildCollectionChange dataclass

What a nested write did to one reverse-FK child collection.

Carried in :attr:~rest_framework_services.ChangeResult.children, one entry per children= relation. The tuples hold child primary keys:

  • created — children inserted.
  • updated — existing children whose row was updated (matched by the :class:~rest_framework_services.ChildSpec's match_key).
  • deleted — orphaned children removed because their FK is non-nullable.
  • unlinked — orphaned children detached (FK set to None) because their FK is nullable.

updated records every matched child the helper ran through update_from_input, regardless of whether that child's own columns actually changed.

UNSET

unset

The UNSET sentinel and its type.

Used to distinguish "field omitted from input" from "field explicitly set to None". Critical for partial updates where None must not stomp on an existing value.

UNSET is the singleton value you compare against (value is UNSET). UnsetType is its type, exported so callers can spell it in annotations — e.g. bio: str | None | UnsetType.

UnsetType

Singleton sentinel type. Always falsy; identity-equal to itself only.

Don't instantiate this directly — use the module-level UNSET singleton. UnsetType() returns that same instance, but the sentinel is the value you compare against and UnsetType is only useful as a type annotation.

NoInput

NoInput

Sentinel type for the InputT slot when a service expects no body.

Pair with :class:DeleteService when the spec has no input_serializer::

@implements(DeleteService[NoInput, Author, None])
def delete_author(
    *,
    instance: Author,
    **extras: Any,
) -> None: ...

The class itself is never instantiated — it exists purely to bind the InputT type variable in a way that is searchable in IDEs and docs.

HttpExtras

HttpExtras

Bases: TypedDict, Generic[UserT]

OfflineContext

OfflineContext dataclass

The HTTP-surrogate a spec needs when dispatched off the HTTP path.

Produced by :func:~rest_framework_services.dispatch.build_offline_context. Its request / view feed the optional request= / view= arguments of :func:~rest_framework_services.dispatch_spec, and the whole value is consumed by :func:~rest_framework_services.enforce_permissions.

  • user — the acting principal (request.user is set to the same value); Any because the user model is project-defined.
  • request — the synthetic DRF Request (.user set, .data carrying the params), forwarded to spec callables that declare request.
  • view — the :class:OfflineServiceView forwarded to callables that declare view and used as the view argument of permission checks.

InputRequired

input_required

The InputRequired schema marker and its type.

Marks a declared input as required in the generated schema without making it required in the type system. Use it inside Annotated[...] on an extras TypedDict key (or an ordinary parameter) of a service / selector::

class WidgetExtras(HttpExtras[MyUser], total=False):
    project_pk: Annotated[int, InputRequired]

@implements(ListSelector[Widget])
def list_widgets(**extras: Unpack[WidgetExtras]) -> list[Widget]:
    return Widget.objects.filter(project_id=extras["project_pk"])

Why a marker and not a required TypedDict key. Under PEP 692, a required key in Unpack[<TypedDict>] makes the function reject callers that omit it, which breaks assignability to :class:~rest_framework_services.ListSelector / :class:~rest_framework_services.RetrieveSelector / the service Protocols — the very reason :class:~rest_framework_services.HttpExtras mandates total=False. So the two things a consumer wants — Protocol conformance and an honest schema — are mutually exclusive through the TypedDict's own totality. Annotated metadata carries no typing weight, so the key stays NotRequired to the type checker while :func:spec_to_json_schema lists it in required.

The marker is advertisement plus enforcement, never delivery: it does not change the kwargs pool, the view.kwargs-over-params precedence, or the SPREAD_AUTHOR_WINS author-beats-client rule. It tells a schema-driven caller (an MCP client, an LLM tool call) that the input is mandatory, and makes dispatch_spec raise :class:~rest_framework_services.ServiceValidationError when it is absent — instead of the bare KeyError the callable would otherwise raise from deep inside dispatch, which no transport maps to a useful error.

Its counterpart is :data:~rest_framework_services.NotClientInput, which hides a key from the schema entirely. A key marked with both is a contradiction and raises at schema-generation time.

InputRequired is the singleton you place in the annotation. InputRequiredType is its type, exported only so the singleton can be spelled in annotations; you never need to instantiate it.

InputRequiredType

Singleton marker type. Identity-equal to itself only.

Don't instantiate this directly — use the module-level InputRequired singleton. InputRequiredType() returns that same instance.

NotClientInput

not_client_input

The NotClientInput schema marker and its type.

Marks a declared input as provider-owned: it is dropped from the generated schema entirely, so a schema-driven caller never learns it exists and never supplies it. Use it inside Annotated[...] on an extras TypedDict key (or an ordinary parameter) of a service / selector::

class WidgetExtras(HttpExtras[MyUser], total=False):
    project_pk: Annotated[int, InputRequired]
    team_role: Annotated[str, NotClientInput]  # resolved by spec.kwargs

Why. Reflecting Unpack[<TypedDict>] into the input schema makes every declared key visible to an LLM / MCP client — including keys a spec.kwargs provider supplies from request state, which the caller has no business setting. Advertising those is merely a wart on the scoping keys, because the selector default SPREAD_AUTHOR_WINS plus an always-resolving provider makes a client-supplied value dead on arrival. NotClientInput removes the wart at the source rather than relying on that invariant to absorb it.

The marker is advertisement-only, never delivery or enforcement: a marked key is still spread into the kwargs pool exactly as before, and the provider still resolves it. It also does not make the key safe on its own — the security property is still the author-wins precedence documented in resolve_provider. Marking a key hidden while opting into SPREAD_CALLER_WINS on a scoped spec voids that guarantee just as it did before, and a provider owning a scoping key must still never decline via UNSET.

A key marked NotClientInput is also excluded from :func:declared_input_keys, so UnknownArguments.REJECT treats a caller that supplies it as passing an unknown argument — which is exactly what it is.

Its counterpart is :data:~rest_framework_services.InputRequired, which marks a key mandatory. A key marked with both is a contradiction and raises at schema-generation time.

NotClientInput is the singleton you place in the annotation. NotClientInputType is its type, exported only so the singleton can be spelled in annotations; you never need to instantiate it.

NotClientInputType

Singleton marker type. Identity-equal to itself only.

Don't instantiate this directly — use the module-level NotClientInput singleton. NotClientInputType() returns that same instance.

UrlKwarg

UrlKwarg dataclass

A URL route capture exposed as a caller-supplied argument off-HTTP.

Over HTTP a nested route's captures (the project_pk of /projects/{project_pk}/widgets/) reach a spec through view.kwargs — directly, or through a spec.kwargs provider that scopes by them. Off-HTTP there is no route, so the caller supplies the value as an ordinary argument: the transport advertises it in the tool / operation schema, pops it out of the arguments, and hands it to build_offline_context(kwargs=…), from where :func:~rest_framework_services.dispatch_spec spreads it into the selector / target pools — authoritative over the spec params, below a spec.kwargs provider. It never reaches the spec as an ordinary input, so the unknown-argument policy never flags it.

This type is declared here, not in each adapter, on purpose. It is the same declaration whichever transport carries it, and two independent copies had already drifted into validating the same declaration against different reserved-name sets. Adapters import it and pair it with :func:~rest_framework_services.validate_channel_names.

Reach for a UrlKwarg when the value is a URL-derived input a spec depends on that is not already an ordinary argument — most commonly a scoping spec.kwargs provider reading view.kwargs (off-HTTP that mapping is otherwise empty, so the provider mis-scopes for every caller), or a closed-surface spec whose route capture must be caller-suppliable.

A selector that reads the value from its own **extras: Unpack[TypedDict] needs no UrlKwarg: drf-services reflects the key into the schema and params delivers it. A key can be both reflected and registered — the explicit UrlKwarg wins the adapter's schema merge, registration pops the argument into kwargs=, and the authoritative spread still delivers it to the selector pool, so both readers see it.

  • name — the argument / view-kwarg key. Must not collide with a reserved transport key; see :func:~rest_framework_services.validate_channel_names.
  • type — the JSON-Schema type advertised to the caller ("string" by default; "integer" / "number" / "boolean" …).
  • description — optional help text shown to the caller.
  • default — optional value seeded when the caller omits the argument; also surfaced as the schema default.
  • required — advertise the key in the schema's required list. Use it for a route capture the spec genuinely cannot run without, so a caller is told up front instead of failing mid-dispatch. Setting both required and a default is contradictory and raises in :func:~rest_framework_services.validate_channel_names.

required here is the registered-declaration counterpart of the :data:~rest_framework_services.InputRequired marker, which does the same job for a key the callable's own TypedDict declares. Both end up in the schema's required; they differ only in where the key is declared.

json_schema

json_schema() -> dict[str, Any]

The JSON-Schema property this kwarg contributes to an input schema.

QueryParam

QueryParam dataclass

A request-level query param exposed as a caller-supplied argument off-HTTP.

Generalizes the built-in page / limit / order list-selector arguments to any read-shaping param a serializer reads off request.query_params — django-restql field selection (?query= / ?fields=), or a custom serializer that branches on the query string. The transport advertises it, pops it from the arguments, and hands it to build_offline_context(query_params=…); it never reaches the spec as an input, so the unknown-argument policy never flags it.

A :class:~rest_framework_services.SelectorSpec filter_set does not need this — its fields are already generated into the schema and flow through as ordinary params.

Declared here rather than in each adapter for the same reason as :class:~rest_framework_services.UrlKwarg: it is the same declaration whichever transport carries it. Pair it with :func:~rest_framework_services.validate_channel_names.

  • name — the argument / query-string key. Must not collide with a reserved transport key; see :func:~rest_framework_services.validate_channel_names.
  • type — the JSON-Schema type advertised to the caller ("string" by default; "integer" / "number" / "boolean" / "array" …).
  • description — optional help text shown to the caller.
  • default — optional value seeded when the caller omits the argument; also surfaced as the schema default.

No required flag, deliberately. A query param is read-shaping — omitting one is legitimate by construction, and the spec runs correctly without it. Requiredness belongs to inputs the spec cannot run without, which is :class:~rest_framework_services.UrlKwarg and the :data:~rest_framework_services.InputRequired marker.

json_schema

json_schema() -> dict[str, Any]

The JSON-Schema property this param contributes to an input schema.

validate_channel_names

validate_channel_names

validate_channel_names(
    *,
    label: str,
    kind: str,
    declarations: Sequence[_ChannelDeclaration],
    reserved: frozenset[str] = frozenset(),
) -> None

Raise ImproperlyConfigured on a bad channel declaration set.

A :class:~rest_framework_services.UrlKwarg / :class:~rest_framework_services.QueryParam is popped out of the caller's arguments and routed to a side channel, so its name must not collide with a key the transport controls, and must not be declared twice.

Three failure modes, all caught at registration time rather than on a call:

  • Reserved-name collision. :data:RESERVED_POOL_SEEDS is always included — those are the dispatcher's authoritative seeds, and letting a caller route a value onto one is the spoofing footgun the spread modes strip. reserved adds the transport's own keys on top; pass the pagination names the transport reserves (page / limit and whichever of order / ordering it uses), since those genuinely differ per transport while the seed set does not.
  • Duplicate names within the set — the later declaration would silently shadow the earlier one.
  • required together with a default — contradictory: a default means the argument can always be satisfied without the caller, so demanding it is either a no-op or a lie. Only checked on declarations that carry a required attribute (QueryParam deliberately has none).

Adapters should call this once per tool / operation, with label identifying the offending registration site and kind naming the parameter the consumer passed ("url_kwargs", "query_params"), so the message points at something the consumer can act on.

RESERVED_POOL_SEEDS

reserved_pool_seeds

RESERVED_POOL_SEEDS — pool keys carrying transport-controlled seeds.