Oaskit
Oaskit is an OpenAPI 3.1 library for Elixir and Phoenix: spec generation, request validation and casting, built on JSON Schema 2020-12.
It provides macros and plugs to automatically validate incoming HTTP requests against the OpenAPI Specification v3.1.
- Request bodies, path, query and header parameters validation with JSON Schema 2020-12, powered by JSV, a validator that passes the official JSON Schema test suite.
- Full OpenAPI 3.1 support:
type: ["string", "null"]instead ofnullable: true,oneOf/anyOf/allOf,$refand$defsbehave as the JSON Schema specification says. - Schemas are modules, plain Elixir maps, or JSON files: anything JSV accepts.
- Response validation helpers for your tests.
- Mix task for JSON file specification generation, and a controller to serve the spec and a Redoc UI.
- Heavily inspired by OpenApiSpex, which targets OpenAPI 3.0. Oaskit is the choice when you need OpenAPI 3.1.
Documentation
The full documentation is available on hexdocs:
Installation
defp deps do
[
{:oaskit, "~> 0.16"},
]
end
You can also import formatter rules in your .formatter.exs file:
[
import_deps: [:oaskit]
]
Example
A condensed tour. The Quickstart Guide walks through each step in more detail.
The spec module
The spec module is the root of your OpenAPI document. Paths are collected from the Phoenix router.
defmodule MyAppWeb.ApiSpec do
alias Oaskit.Spec.Paths
alias Oaskit.Spec.Server
use Oaskit
@impl true
def spec do
%{
openapi: "3.1.1",
info: %{title: "My App API", version: "1.0.0"},
servers: [Server.from_config(:my_app, MyAppWeb.Endpoint)],
paths: Paths.from_router(MyAppWeb.Router, filter: &String.starts_with?(&1.path, "/api/"))
}
end
end
Router and controllers
Declare the spec in a router pipeline, and add the validation plug to your controllers.
# router.ex
pipeline :api do
plug :accepts, ["json"]
plug Oaskit.Plugs.SpecProvider, spec: MyAppWeb.ApiSpec
end
scope "/api", MyAppWeb do
pipe_through :api
get "/users", UserController, :index
post "/users", UserController, :create
patch "/users/:id", UserController, :update
end
# my_app_web.ex
def controller do
quote do
use Phoenix.Controller, formats: [:json]
use Oaskit.Controller
plug Oaskit.Plugs.ValidateRequest
# ...
end
end
Schemas
Oaskit validates with JSV, so a schema can be a module, a plain Elixir map, or a JSON document decoded from a file.
A schema module defined with JSV.defschema/3 is referenced by name, and
valid request bodies are cast to its struct. Properties are required unless
wrapped with optional/1, and the struct can be encoded to JSON.
defmodule MyAppWeb.Schemas do
use JSV.Schema
defschema User,
name: non_empty_string(),
email: email(),
# OpenAPI 3.1: a type union, no more `nullable: true`
nickname: optional(%{type: [:string, :null]})
end
An inline schema is a plain Elixir map. For request bodies and responses,
wrap it in a {schema, options} tuple.
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias MyAppWeb.Schemas.User
# Parameters are validated and cast: `limit` is an integer here
operation :index,
parameters: [
limit: [in: :query, schema: %{type: :integer, minimum: 1, maximum: 100}]
],
responses: [ok: {%{type: :array, items: User}, []}]
def index(conn, _params) do
users = MyApp.Users.list(limit: query_param(conn, :limit, 20))
json(conn, users)
end
# Using a schema module
operation :create,
request_body: User,
responses: [created: User]
def create(conn, _params) do
%User{} = user = body_params(conn)
# ...
end
# Using an inline schema
operation :update,
parameters: [id: [in: :path, schema: %{type: :integer}]],
request_body:
{%{
type: :object,
properties: %{email: %{type: :string, format: :email}},
required: [:email],
additionalProperties: false
}, required: true},
responses: [ok: User]
def update(conn, _params) do
id = path_param(conn, :id)
%{"email" => email} = body_params(conn)
# ...
end
end
Invalid requests are rejected with a response describing the errors: 400 for
invalid parameters, 422 for an invalid body and 415 for an unsupported
content type. Errors are rendered as JSON, or as an HTML page when the Accept
header contains html (disable this with the html_errors: false option). They
can be rendered your own way with the :error_handler option of
Oaskit.Plugs.ValidateRequest.
Testing responses
Oaskit.Test.valid_response/3 checks the status, content type and body of a
response against your spec, and returns the decoded body.
test "create user", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(~p"/api/users", %{name: "Alice", email: "alice@example.com"})
assert %{"name" => "Alice"} = Oaskit.Test.valid_response(MyAppWeb.ApiSpec, conn, 201)
end
Generating and serving the spec
Write the spec to a file, for client generators or CI checks:
mix openapi.dump MyAppWeb.ApiSpec --pretty -o priv/openapi.json
Or serve it, with a Redoc UI:
get "/openapi.json", Oaskit.SpecController, spec: MyAppWeb.ApiSpec
get "/docs", Oaskit.SpecController, redoc: "/openapi.json"
Contributing
Pull requests are welcome, provided they include appropriate tests and documentation.
Roadmap
- Serve Swagger UI.
- Allow custom formatters for the
openapi.dumpMix task, to support other output formats such as YAML.