Request Validation with Oaskit
View SourceA spec module (use AshOaskit) implements the Oaskit behaviour, which
unlocks oaskit's request validation machinery. This guide is honest about
the scope: what is validated, by whom, and where oaskit's plugs apply.
Who validates what
| Routes | Validated by |
|---|---|
Ash-served routes (AshJsonApi.Router) | AshJsonApi itself — actions validate their inputs, the JSON:API layer checks document structure |
| Hand-written Phoenix controllers | Oaskit.Plugs.ValidateRequest against your spec module |
Oaskit.Plugs.ValidateRequest resolves operations through the
operation macro from use Oaskit.Controller, which requires a Phoenix
controller per route. AshJsonApi serves its routes through a forwarded
plug router without per-route Phoenix controllers, so ValidateRequest
cannot intercept Ash-served routes — and it does not need to: Ash
already validates those requests at the action layer.
Where the integration shines is hybrid APIs: hand-written endpoints documented in the same spec as your Ash routes get full request validation against the schemas you declare.
Setting up validation for hand-written controllers
- Provide the spec module to the pipeline:
# router.ex
pipeline :api do
plug :accepts, ["json"]
plug Oaskit.Plugs.SpecProvider, spec: MyAppWeb.ApiSpec
end
scope "/api", MyAppWeb do
pipe_through :api
post "/reports", ReportController, :create
end- Declare the operation in the controller and validate:
defmodule MyAppWeb.ReportController do
use MyAppWeb, :controller
use Oaskit.Controller
plug Oaskit.Plugs.ValidateRequest
operation :create,
operation_id: "create_report",
request_body: {%{
"type" => "object",
"required" => ["name"],
"properties" => %{"name" => %{"type" => "string"}}
}, []},
responses: [ok: true]
def create(conn, _params) do
%{"name" => name} = body_params(conn)
json(conn, %{"name" => name})
end
endInvalid requests are rejected before your action runs, with structured errors from oaskit's default error handler.
Merging hand-written operations into the spec
Operations declared with the operation macro live on the controller.
To document them in your AshOaskit spec output, pass your Phoenix
router via the :router option of use AshOaskit — controllers
implementing AshOaskit.OpenApiController are introspected and merged
into paths.
Validating responses in tests
Oaskit.Test.valid_response/3 asserts a conn's response against the
spec's response schema for the matched operation (it requires the route
to have gone through ValidateRequest, so it applies to the same
hand-written controllers):
use MyAppWeb.ConnCase, async: true
import Oaskit.Test
test "create report returns a valid response", %{conn: conn} do
conn = post(conn, ~p"/api/reports", %{"name" => "Q3"})
assert %{"name" => "Q3"} = valid_response(MyAppWeb.ApiSpec, conn, 200)
endFor Ash-served routes, assert against the generated spec directly — the spec is data:
test "generated spec stays valid" do
assert {:ok, _} = AshOaskit.validate(MyAppWeb.ApiSpec.spec())
endValidating the spec itself
Two layers are available and cheap to run in CI:
# Structural validation against the OpenAPI metaschema
{:ok, %Oaskit.Spec.OpenAPI{}} = AshOaskit.validate(MyAppWeb.ApiSpec.spec())
# Full build: normalization + JSV validator construction for every operation
Oaskit.build_spec!(MyAppWeb.ApiSpec)Oaskit.build_spec! is the stronger check — it proves every schema in
the spec compiles to a working JSV validator.