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 | SelectorSpec | SpecSource,
    *,
    overrides: Mapping[str, FieldMarking] | None = None,
    name: str | None = None,
) -> AudienceProjection

The AudienceProjection of what spec presents.

django-service-specs' audience_projection_for_spec: the field markings of the compiled spec's output, with overrides laid over them, and name naming the projection in a refusal. A SelectorSpec with no selector is projected from its declaration, since projecting runs no selector.

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 output declaration'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.

serializer_cls is read as the class it renders through, resolved with renderable_serializer_class, so a bare @dataclass output is projected from the DataclassSerializer it is rendered with. Two consequences follow, both deliberate. A marking given to a dataclass field — field(metadata={"serializer_kwargs": {"style": {MARKING: FieldMarking.hidden()}}}) — is collected like one on a serializer field, so hidden means hidden whichever way the output was declared. And an Enum field renders as a ChoiceField whose display values are the member names, so an agent reads "GOLD" where the browser reads "gold": the substitution every ChoiceField gets, and mirrored into the schema by annotate_output_schema. Mark the field HANDLE to keep the value for an agent too.

None, or anything else that does not resolve to a DRF Serializer, yields an empty projection rather than an error: there is nothing to mark up.

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, speak choice displays and format values, at any depth.

Three changes, each driven by the declaration that shapes the schema in annotate_output_schema, so the two cannot disagree:

  • fields marked HIDDEN are removed. Removed rather than moved under some reserved key: a payload a model reads is read in full either way, so moving a field costs its keys again and hides nothing. What an agent must never use should not be there.
  • a field's choice value is replaced by its display, so a person is not read PENDING_REVIEW. Not on a HANDLE, which another tool takes as input. An array of choices is substituted member by member rather than looked up whole.
  • a field whose marking carries 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. It wins over the choice display, being the transform an author wrote by hand, and over a nested output's projection. Also not on a HANDLE, for the same reason, and a null is never formatted.

A payload that is neither an object nor a list of them is returned as it is, and so is every key the projection has nothing to say about, including one a transport added beside the declared fields.

Apply this where a payload becomes an agent's answer, not where it feeds the next step of a chain, which still needs the handles. Returns a new payload and never modifies payload.

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 to a schema the projection project_payload applies to the payload.

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

  • hidden properties are removed, and dropped from required, which is left out when nothing in it remains;
  • a marking's description becomes the property's "description", so a handle says what it is for in the schema a model reads without that wording reaching any other reader;
  • a field with labelled choices is restated in its displays, because that is what the projected payload carries: an enum's values are replaced, and a oneOf's consts are, each losing the title that annotated the value it now equals. A "type" stated beside them is restated as the types of the values now listed, so a display string is never described as the integer it replaced, with "null" named last where the stated type admitted it and a null is still served - not where the choices give None a display, which is served in its place - and left as written beside a oneOf entry that admits more than its constants. A display two values share is listed once, so a row served it matches one oneOf entry rather than two, and an array of such choices no longer claims uniqueItems. Not on a HANDLE, which keeps its values on both sides: a field another tool takes as input should be marked one.
  • a formatted field is restated as the type its ValueFormatter says it produces, plus whatever that declaration adds about the shape of the produced value, in place of everything the property said about the value before, choices included. Its title and description are kept, and so is a null the property admitted - however it stated one, as a type, an anyOf member, None in an enum or a oneOf entry for a labelled choice - since a null is never formatted. The type is written from produces rather than taken from the fragment, so a renderer cannot contradict its own advertisement, and a fragment's enum lists the null too, and its const becomes an enum of that value and a null, where the property admitted one. Not on a HANDLE either.

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 wording for a HANDLE whose marking declares none, and defaults to nothing. Telling a reader what to do with an identifier is advice for one kind of reader, and the kernel does not know which kind is reading. The transport that knows its audience supplies the sentence.

Takes the item schema; an array's items are annotated and the array kept. A caller wrapping items in an envelope of its own annotates the item and wraps it afterwards, as spec_output_schema does. None, or a projection with nothing in it, returns schema itself; anything else returns a new schema and never modifies schema.