Skip to content

Audience projection

One serializer, more than one kind of reader. These helpers let a field say who it is written for — see the recipe for the task-shaped version — and let an agent transport apply that declaration to payloads and schemas from the same source, so the two cannot disagree.

Nothing here is read by the DRF view path. A serializer marked up for an agent renders byte-identically behind a viewset.

Nor is there any wording for a model here. The markings say what a field is; what a reader should do about it depends on the reader, and the transport is what knows — so annotate_output_schema takes the sentence rather than supplying one.

The value types (FieldAudience, FieldMarking, AudienceProjection, MARKING) are documented under Types; the render entry points (render_for_audience / arender_for_audience) under Dispatch.

audience_projection_for_spec

audience_projection_for_spec

audience_projection_for_spec(
    spec: ServiceSpec[Any, Any, Any] | SelectorSpec[Any, Any],
    *,
    overrides: Mapping[str, FieldMarking] | None = None,
    name: str | None = None,
) -> AudienceProjection

Resolve the field markings on whatever serializer spec renders through.

A selector keeps it on output_serializer; a service keeps it one level down, on output_selector_spec. A transport that registers its tools up front calls this once per spec and hands the result to render_for_audience, rather than paying a serializer instantiation on every call — and rather than each transport re-deriving where a spec keeps its output serializer.

overrides and name are build_audience_projection's, forwarded: a mount holding an OfflineContract passes its field_audiences straight through, and every agent transport layers that one declaration by the same rule.

build_audience_projection

build_audience_projection

build_audience_projection(
    serializer_cls: type | None,
    *,
    overrides: Mapping[str, FieldMarking] | None = None,
    name: str | None = None,
) -> AudienceProjection

Resolve one serializer class's agent presentation, recursing into children.

Reads FieldMarking markings out of each field's style bag and collects the ChoiceField labels DRF already holds. Pure in the serializer class, so the result is built once and reused — see AudienceProjection.

A class that is not a DRF serializer (a plain @dataclass output, or None) yields an empty projection rather than an error: not every spec renders through a serializer, and nothing is marked up in those that don't.

overrides layers a caller's markings on top, for the one case the serializer cannot express: a mount that needs what its sibling hides. They reach a transport as OfflineContract.field_audiences, which is one declaration read by every agent transport — and the merge lives here, next to the clash rule it extends, so that stays true. Two copies of it is how one spec comes to project a different field set depending on which transport served it. name identifies the mount in the error message and defaults to the serializer's own class name.

Raises:

Type Description
ImproperlyConfigured

If a field carries something other than an FieldMarking under MARKING, or if two fields both claim LABEL — whether the serializer declared the clash or an override introduced it. Neither can be caught later: the first would silently do nothing, and the second would silently pick one.

project_payload

project_payload

project_payload(payload: Any, projection: AudienceProjection) -> Any

Drop plumbing and speak enum labels, at any depth.

Two changes, both driven by the same declaration that shapes the schema so the two cannot disagree:

  • fields marked HIDDEN are removed. Removed rather than nested under some reserved subtree: a payload that is also emitted as text is read in full either way, so relocating a field costs its keys again and hides nothing. What an agent must never use should not be there.
  • a ChoiceField's constant is replaced by its display value, so the enum does not have to be spelled out to a person as PENDING_REVIEW. Not on a HANDLE, which some other tool takes as input. A MultipleChoiceField renders a collection of constants, so each member is substituted rather than the collection looked up whole.
  • a field carrying a ValueFormatter is rendered through it — a date-time read as a local date-time rather than raw ISO-8601, an amount with its currency. Also not on a HANDLE, and for the same reason.

Apply this where a payload becomes the agent's answer. Not where it feeds the next step of a chain, which still needs the handles.

annotate_output_schema

annotate_output_schema

annotate_output_schema(
    schema: dict[str, Any] | None,
    projection: AudienceProjection,
    *,
    handle_description: str | None = None,
) -> dict[str, Any] | None

Apply the same projection to a schema that project_payload applies to the payload.

Three changes, each the mirror of one the payload undergoes:

  • hidden properties are removed, and dropped from required;
  • a marked field's description replaces the help_text one, so a handle says what it is for in the schema a model reads without that wording leaking into the browsable API;
  • a substituted choice field is re-declared in terms of its display values, because that is what the projected payload now carries. The constant is gone from the response by design — a field another tool takes as input should be marked HANDLE, which suppresses the substitution on both sides.
  • a formatted field is re-declared as the type its ValueFormatter says it produces, plus whatever that declaration adds about the shape of the produced value. The framework writes the type from produces rather than taking one from the fragment, so a renderer cannot contradict its own advertisement.

Generating both sides from one declaration is the point: a schema that advertises a field the payload no longer carries is worse than either behaviour on its own.

handle_description is the fallback wording for a HANDLE that declares none of its own, and defaults to nothing. Telling a reader what to do with an identifier is advice for one kind of reader, and this package does not know which kind is reading — a CSV export has no use for it, and "do not read this out" only means something to a consumer that reads things out. The transport that knows its audience supplies the sentence.

Takes the item schema. Callers that wrap items in an envelope of their own — an array, or a pagination object — annotate the item and wrap afterwards; output_to_json_schema(projection=...) does exactly that.