JSON Schema¶
View-free JSON Schema generation. These helpers turn a ServiceSpec /
SelectorSpec (or a bare DRF serializer / dataclass) into a JSON Schema
dict, with no view, request, or drf-spectacular dependency — what an
alternate transport (a Pydantic-AI toolset, the MCP server) builds tool
definitions from. Distinct from the OpenAPI adapter, which
produces DRF serializer classes for DRF's own OpenAPI generators.
serializer_to_json_schema¶
serializer_to_json_schema ¶
serializer_to_json_schema(
serializer: type | None,
*,
partial: bool = False,
registry: JsonSchemaRegistry = DEFAULT_JSON_SCHEMA_REGISTRY,
) -> dict[str, Any]
Build a JSON Schema object for an input serializer / dataclass / None.
Accepts a DRF Serializer subclass, a bare @dataclass type (the
convention drf-services services use for data), or None (the
operation takes no input). Always returns an object — {"type": "object"}
is the convention for "no declared fields", so an alternate transport can
still describe the tool.
partial=True drops the required list — mirroring spec.partial,
where the validator accepts omitted fields, so advertising them as required
would make schema-strict consumers reject calls the service accepts.
registry supplies consumer rules for custom field / Python types — see
:class:~rest_framework_services.JsonSchemaRegistry.
output_to_json_schema¶
output_to_json_schema ¶
output_to_json_schema(
output_serializer: type | None,
*,
kind: SelectorKind | None = None,
paginate: bool = False,
registry: JsonSchemaRegistry = DEFAULT_JSON_SCHEMA_REGISTRY,
) -> dict[str, Any] | None
Build a JSON Schema for an output serializer, or None when undeclared.
Returns None when there is no output_serializer — callers shouldn't
fabricate a misleading shape. kind / paginate make the schema match
what dispatch actually returns:
kind=None/RETRIEVE— the bare item schema.kind=LIST, paginate=False—{type: array, items: <item>}.kind=LIST, paginate=True— the pagination envelope{items, page, totalPages, hasNext}.
registry supplies consumer rules for custom field / Python types — see
:class:~rest_framework_services.JsonSchemaRegistry.
filterset_to_json_schema¶
filterset_to_json_schema ¶
filterset_to_json_schema(
filter_set_class: Any,
*,
registry: JsonSchemaRegistry = DEFAULT_JSON_SCHEMA_REGISTRY,
) -> dict[str, dict[str, Any]]
Map a django-filter FilterSet class to JSON Schema properties.
Returns a dict shaped like the "properties" key of a JSON Schema object
— ready to merge into a spec's input schema (see
:func:~rest_framework_services.spec_to_json_schema, which does this for a
SelectorSpec carrying a filter_set). Every filter is optional: a
filter narrows the queryset but is never required to call the operation, so
no name is added to a required array.
registry.filters rules are tried first (so a consumer can map a custom
filter type or override a built-in); otherwise common filter classes get
accurate mappings and anything unrecognised falls back to {} (JSON Schema
"any value") so a custom filter never breaks generation. Reads
FilterSet.base_filters directly (populated by the metaclass at
class-creation time) so the FilterSet isn't instantiated.
Requires the [filter] extra (django-filter). The core never imports
it — SelectorSpec.filter_set is applied by duck typing — so the import is
function-local here and raises a clear error when the extra is absent. This
only fires when a filter_set is actually introspected, so projects that
don't use one are unaffected.
spec_to_json_schema¶
spec_to_json_schema ¶
spec_to_json_schema(
spec: ServiceSpec[Any, Any, Any] | SelectorSpec[Any, Any],
*,
phase: Literal["input", "output"] = "input",
registry: JsonSchemaRegistry = DEFAULT_JSON_SCHEMA_REGISTRY,
) -> dict[str, Any] | None
Derive a JSON Schema from a spec, reading the right serializer off it.
The convenience an alternate transport (a Pydantic-AI toolset, the MCP server) calls instead of reaching into spec internals itself.
phase="input" (default) returns the input-argument schema:
- :class:
ServiceSpec→ itsinput_serializer(spec.partialhonoured). - :class:
SelectorSpec→ an object whosepropertiescombine the selector callable's own parameters (names → type from their annotations, skipping therequest/user/viewtransport seeds) with itsfilter_setfields (viafilterset_to_json_schema); a bare{"type": "object"}when it exposes neither. So a retrieve selector likeget_widget(user, pk)now advertisespkinstead of leaning entirely on its docstring. A**kwargs: Unpack[SomeExtras]parameter is expanded into one property perTypedDictkey, with the TypedDict's required keys populatingrequired— so a URL kwarg a selector reads fromextras(a nested route'sparent_pk) is discoverable off-HTTP rather than a hiddenKeyError. (Introspecting afilter_setneeds the[filter]extra; a selector without one stays dependency-free.)
phase="output" returns the output schema, or None when undeclared:
- :class:
ServiceSpec→ itsoutput_selector_spec'soutput_serializerandkind. - :class:
SelectorSpec→ its ownoutput_serializerandkind.
registry supplies consumer rules for custom field / filter / Python types
— see :class:~rest_framework_services.JsonSchemaRegistry.
A selector's input schema reflects the selector callable's own parameters —
names plus a JSON type from each annotation, skipping the request / user /
view transport seeds — merged with its filter_set fields. So a lookup selector
like get_widget(user, pk) advertises pk instead of a bare {"type": "object"}
that leans on the docstring alone. An un-annotated parameter is still surfaced by
name (untyped {}); a filter_set field wins over a callable parameter of the
same name.
JsonSchemaRegistry¶
JsonSchemaRegistry
dataclass
¶
Consumer-extensible type → JSON Schema fragment rules for the helpers.
Three ordered rule lists, each a sequence of (type, schema_fragment):
fields— DRFFieldsubclasses, matched byisinstancewhen walking a serializer (serializer_to_json_schema/ the input side ofspec_to_json_schema).filters—django-filterFiltersubclasses, matched byisinstancewhen walking aFilterSet(filterset_to_json_schema).python_types— bare Python types, matched by identity when walking a dataclass's field annotations.
Rules are tried in order and the first match wins, before the built-in mappings — so a rule both adds support for a custom type and can override a built-in. The matched fragment is copied per use, so callers may freely mutate the returned schema.
Immutable: :meth:extend returns a new registry rather than mutating, so
there is no shared global state to leak across callers or tests. Start from
:data:DEFAULT_JSON_SCHEMA_REGISTRY (empty) and layer rules on::
registry = DEFAULT_JSON_SCHEMA_REGISTRY.extend(
fields=[(MoneyField, {"type": "string", "format": "money"})],
)
schema = serializer_to_json_schema(MySerializer, registry=registry)
The built-in mappings are intentionally not held here — they live with the
walkers (and the filter built-ins import django-filter lazily). A
registry carries only consumer additions, so the empty default stays
dependency-free.
extend ¶
extend(
*,
fields: Sequence[_Rule] = (),
filters: Sequence[_Rule] = (),
python_types: Sequence[_Rule] = (),
) -> JsonSchemaRegistry
Return a new registry with the given rules prepended (they win first).