Openapi
A lightweight OpenAPI-first routing, validation and documentation layer for Elixir/Phoenix applications.
It parses OpenAPI (YAML/JSON) definitions, generates Phoenix routes, optionally validates requests using JSON Schema, and provides built-in Swagger UI integration for interactive API documentation.
Usage
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use Openapi.Phoenix
pipeline :api do
plug :accepts, ["json"]
end
scope "/" do
pipe_through :api
openapi "priv/swagger.yaml"
swagger_docs "/api-docs"
end
end
The openapi macro is responsible for turning an OpenAPI definition into live Phoenix routes.
When used in a router, it performs the following steps at compile time:
- Reads the provided OpenAPI file (
.yaml,.yml, or.json) - Normalizes all paths to Phoenix format (e.g.
/user/{id}→/user/:id) - Generates Phoenix routes for every HTTP method defined under each path
- Attaches routing metadata for later dispatch
At runtime, requests are dispatched based on the generated metadata:
- If a global
handleroption is provided, it is used as the default module - Otherwise, the macro uses the per-operation
x-handlervalue from the OpenAPI file - The
operationIddetermines the function to call inside the handler module
Request validation
Openapi.ValidatorPlug validates incoming requests against the schemas defined in your
OpenAPI document, using ex_json_schema.
Add it to any pipeline. It validates only the operations that actually define schemas, and passes everything else through untouched:
pipeline :api do
plug :accepts, ["json"]
plug Openapi.ValidatorPlug
end
The schemas are resolved once at compile time and embedded into each route, so validation works in any environment without relying on the spec ever being served.
Given an OpenAPI operation like:
paths:
/pets:
post:
operationId: createPet
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPet'
components:
schemas:
NewPet:
type: object
required: [name]
properties:
name: { type: string }
age: { type: integer }
A request with an invalid body is rejected with a 400 JSON response before reaching your
handler:
{
"errors": [
{"source": "body", "path": "#/name", "message": "Required property name was not present."}
]
}
What gets validated:
- Request body — the
application/jsonrequestBodyschema - Query parameters — declared
parameterswithin: query - Path parameters — declared
parameterswithin: path
Path and query parameters arrive as strings; the plug coerces them to the declared type
(integer, number, boolean) before validating.
Options
:validate— which parts to validate. Defaults to[:body, :query, :path]:# Only validate request bodies, skip paramsplug Openapi.ValidatorPlug, validate: [:body]:on_error— afun(conn, errors)returning aPlug.Conn, used to customize the failure response. Defaults to a400JSON response with error details:plug Openapi.ValidatorPlug, on_error: &MyApp.Errors.handle_validation/2
Telemetry
openapi emits Telemetry span events at key points in the
request lifecycle, following the same convention as Phoenix. Attach a handler once at
application startup and you get timing, operation identity, and error information for every
OpenAPI-routed request.
Validation events
Emitted by Openapi.ValidatorPlug around request schema validation:
| Event | When |
|---|---|
[:openapi, :request, :validation, :start] | Before validation runs |
[:openapi, :request, :validation, :stop] | After validation completes |
The :stop event metadata includes operation_id, server, and errors — an empty list
when validation passed, a list of error maps when it failed:
:telemetry.attach("log-validation", [:openapi, :request, :validation, :stop], fn _event, _measurements, metadata, _config ->
if metadata.errors != [] do
Logger.warning("Validation failed for #{metadata.operation_id}: #{inspect(metadata.errors)}")
end
end, nil)
Dispatch events
Emitted by Openapi.DispatchPlug around every handler invocation:
| Event | When |
|---|---|
[:openapi, :request, :dispatch, :start] | Before the handler is called |
[:openapi, :request, :dispatch, :stop] | After the handler returns |
[:openapi, :request, :dispatch, :exception] | If the handler raises |
Measurements: system_time (start), duration (stop/exception). Metadata:
%{conn, operation_id, server, handler}.
:telemetry.attach("log-dispatch", [:openapi, :request, :dispatch, :stop], fn _event, measurements, metadata, _config ->
Logger.info("#{metadata.operation_id} dispatched in #{div(measurements.duration, 1_000)}µs")
end, nil)
Response validation
Openapi.ResponseValidatorPlug validates that your handler's response body matches the
schema declared in the spec's responses section for each HTTP status code.
It uses Plug.Conn.register_before_send/2 to inspect the response after the handler runs,
so it never blocks or alters the response — it just calls on_error if there is a mismatch.
Best used in dev and test pipelines to catch spec drift before API consumers do:
# config/dev.exs or a test-only pipeline
pipeline :api do
plug :accepts, ["json"]
plug Openapi.ValidatorPlug
plug Openapi.ResponseValidatorPlug
end
The default on_error logs a warning. In tests you can make it raise to fail fast:
plug Openapi.ResponseValidatorPlug, on_error: fn conn, errors ->
raise "Response mismatch for #{conn.private.openapi.operation_id}: #{inspect(errors)}"
end
Response schemas are compiled at the same time as request schemas — at route-generation time — so there is no runtime spec file dependency.
Installation
The package can be installed by adding openapi to your list of dependencies in mix.exs:
def deps do
[
{:openapi, "~> 0.1.0"}
]
end
Documentation can be generated with ExDoc and is published on HexDocs.
License
MIT