api/controllers/API_SCHEMA_GUIDE.md
This guide describes the expected Flask-RESTX + Pydantic pattern for controller request payloads, query parameters, response schemas, and Swagger documentation.
BaseModel for request bodies and query parameters.fields.base.ResponseModel for response DTOs.fields.* dictionaries, Namespace.model(...) exports, or @marshal_with(...) for migrated or new endpoints.@ns.expect(...) for GET query parameters. Flask-RESTX documents that as a request body.The Console and Web /system-features endpoints share SystemFeatureModel. They are unauthenticated and may be
requested during root SSR, so treat this response as a minimal public bootstrap allowlist. It is not a general
configuration endpoint, a feature registry, or a mirror of environment and Enterprise settings. Existing fields are
legacy inventory and do not establish precedent for new fields.
A new field is eligible only when all of the following are true:
Do not add:
Route excluded values to their actual owner:
Every pull request that adds a System Features field must:
Reviewers should reject a field when its owner, pre-authentication need, or consumers are unclear.
Payload suffix.
WorkflowRunPayload, DatasourceVariablesPayload.Query suffix.
WorkflowRunListQuery, MessageListQuery.Response suffix and inherit from ResponseModel.
WorkflowRunDetailResponse, WorkflowRunNodeExecutionListResponse.ListResponse or PaginationResponse for wrapper responses.
WorkflowRunNodeExecutionListResponse, WorkflowRunPaginationResponse.fields/*_fields.py only when shared by multiple controllers.Use helpers from controllers.common.schema.
from controllers.common.schema import (
query_params_from_model,
register_response_schema_models,
register_schema_models,
)
from libs.helper import dump_response
Register request payload and query models with register_schema_models(...):
register_schema_models(
console_ns,
WorkflowRunPayload,
WorkflowRunListQuery,
)
Register response models with register_response_schema_models(...):
register_response_schema_models(
console_ns,
WorkflowRunDetailResponse,
WorkflowRunPaginationResponse,
)
Response models are registered in Pydantic serialization mode. This matters when a response model uses
validation_alias to read internal object attributes but emits public API field names. For example, a response model
can validate from inputs_dict while documenting and serializing inputs.
For non-GET request bodies:
Payload model.register_schema_models(...).@ns.expect(ns.models[Payload.__name__]) for Swagger documentation.ns.payload or {} inside the controller.class DraftWorkflowNodeRunPayload(BaseModel):
inputs: dict[str, Any]
query: str = ""
register_schema_models(console_ns, DraftWorkflowNodeRunPayload)
@console_ns.expect(console_ns.models[DraftWorkflowNodeRunPayload.__name__])
def post(self, app_model: App, node_id: str):
payload = DraftWorkflowNodeRunPayload.model_validate(console_ns.payload or {})
result = service.run(..., inputs=payload.inputs, query=payload.query)
return dump_response(WorkflowRunNodeExecutionResponse, result)
For GET query parameters:
Query model.register_schema_models(...) if it is referenced elsewhere in docs, or only use
query_params_from_model(...) if a body schema is not needed.@ns.doc(params=query_params_from_model(QueryModel)).request.args.to_dict(flat=True) or an explicit dict when type coercion is needed.class WorkflowRunListQuery(BaseModel):
last_id: str | None = Field(default=None, description="Last run ID for pagination")
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page (1-100)")
@console_ns.doc(params=query_params_from_model(WorkflowRunListQuery))
def get(self, app_model: App):
query = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
result = service.list(..., limit=query.limit, last_id=query.last_id)
return dump_response(WorkflowRunPaginationResponse, result)
Do not do this for GET query parameters:
@console_ns.expect(console_ns.models[WorkflowRunListQuery.__name__])
def get(...):
...
That documents a GET request body and is not the expected contract.
204 No Content responses must not serialize a response body. Return the status using the established controller pattern;
do not return a dictionary, response model, or other payload.
Response models should inherit from ResponseModel:
class WorkflowRunNodeExecutionResponse(ResponseModel):
id: str
inputs: Any = Field(default=None, validation_alias="inputs_dict")
process_data: Any = Field(default=None, validation_alias="process_data_dict")
outputs: Any = Field(default=None, validation_alias="outputs_dict")
Document response models with @ns.response(...):
@console_ns.response(
200,
"Node run started successfully",
console_ns.models[WorkflowRunNodeExecutionResponse.__name__],
)
def post(...):
...
Serialize explicitly:
return dump_response(WorkflowRunNodeExecutionResponse, workflow_node_execution)
dump_response(...) is the preferred response serialization helper for a single Pydantic response DTO. It validates
with from_attributes=True and returns model_dump(mode="json"), so SQLAlchemy models, plain objects, dictionaries,
Pydantic aliases, computed fields, and datetime values are serialized consistently.
For wrapper responses, pass a dictionary with the public wrapper fields:
return dump_response(
WorkflowRunPaginationResponse,
{
"data": workflow_runs,
"page": page,
"limit": limit,
"has_more": has_more,
},
)
If the service can return None, translate that into the expected HTTP error before validation:
workflow_run = service.get_workflow_run(...)
if workflow_run is None:
raise NotFound("Workflow run not found")
return dump_response(WorkflowRunDetailResponse, workflow_run)
Use manual model_validate(...).model_dump(...) only when the endpoint needs behavior that dump_response(...) does
not provide, such as returning a non-dict payload, intentionally excluding fields, or composing a (body, status) tuple.
Avoid adding these patterns to new or migrated endpoints:
ns.model(...) for new request/response DTOs.workflow_run_detail_model.fields.Nested({...}) with raw inline dict field maps.@marshal_with(...) for response serialization.@ns.expect(...) for GET query params.Existing legacy field dictionaries may remain where an endpoint has not yet been migrated. Keep that compatibility local to the legacy area and avoid importing RESTX model objects from controllers.
For schema and documentation changes, run focused tests and generate Swagger JSON:
uv run --project . pytest tests/unit_tests/controllers/common/test_schema.py
uv run --project . pytest tests/unit_tests/commands/test_generate_swagger_specs.py tests/unit_tests/controllers/test_swagger.py
uv run --project . dev/generate_swagger_specs.py --output-dir /tmp/dify-openapi-check
Inspect affected endpoints with jq. Check that:
in: query.*Response schema.inputs_dict.