`UnprocessableEntity(exc.json())` leaked the version-pinned pydantic docs url
and echoed the entire user-supplied input, and double-encoded JSON into the
`message` field. Use flask-restx `abort(422, message=..., errors=...)` with
`exc.errors(include_url=False, include_input=False, include_context=False)` so
the body is a structured object with no version/input leak and no double-encoding.
Also trim verbose comments/docstrings across the touched openapi files.
Finish unifying request validation so every validation failure on the
openapi surface (except the excluded OAuth device-flow) returns 422:
- migrate workspace member invite (POST) and role update (PUT) and the
human-input form submit (POST) onto @accepts(body=); drop the local
_validate_body helper. human-input previously ran a bare model_validate
with no handler, so a malformed body surfaced as 500 — now a clean 422.
Service-layer domain errors keep their intentional 400s.
- harden @returns to pass (model, status, headers) tuples through verbatim
instead of assuming a 2-tuple.
- stop test_contract from leaking test-only models into the shared
openapi_ns, and assert the contract decorators' Swagger metadata
(params/expect/responses) survives the guard wrapper stack.
Spec-neutral: the migrated handlers already advertised the same expect
models, so the generated swagger/markdown/contracts are unchanged.
Introduce two decorators in controllers/openapi/_contract.py that each own one
slice of the HTTP contract from a single Pydantic model reference:
- @accepts(query=, body=): validate the request, inject the validated model as a
typed kwarg, and emit the Swagger param/body schema. Validation errors map to
422 with ValidationError.json() (one shape).
- @returns(status, model): emit the response schema and serialize the handler's
returned model via model_dump(mode="json"); non-model returns pass through.
Both sit below @auth_router.guard so auth precedes validation and the unit-test
__wrapped__ seam keeps unwrapping exactly the guard layer.