Skip to content

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 → its input_serializer (spec.partial honoured).
  • :class:SelectorSpec → an object whose properties combine the selector callable's own parameters (names → type from their annotations, skipping the request / user / view transport seeds) with its filter_set fields (via filterset_to_json_schema); a bare {"type": "object"} when it exposes neither. So a retrieve selector like get_widget(user, pk) now advertises pk instead of leaning entirely on its docstring. A **kwargs: Unpack[SomeExtras] parameter is expanded into one property per TypedDict key, with the TypedDict's required keys populating required — so a URL kwarg a selector reads from extras (a nested route's parent_pk) is discoverable off-HTTP rather than a hidden KeyError. (Introspecting a filter_set needs the [filter] extra; a selector without one stays dependency-free.)

phase="output" returns the output schema, or None when undeclared:

  • :class:ServiceSpec → its output_selector_spec's output_serializer and kind.
  • :class:SelectorSpec → its own output_serializer and kind.

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 — DRF Field subclasses, matched by isinstance when walking a serializer (serializer_to_json_schema / the input side of spec_to_json_schema).
  • filtersdjango-filter Filter subclasses, matched by isinstance when walking a FilterSet (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).

DEFAULT_JSON_SCHEMA_REGISTRY

DEFAULT_JSON_SCHEMA_REGISTRY module-attribute

DEFAULT_JSON_SCHEMA_REGISTRY: JsonSchemaRegistry = JsonSchemaRegistry()