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:

At runtime, requests are dispatched based on the generated metadata:

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:

Path and query parameters arrive as strings; the plug coerces them to the declared type (integer, number, boolean) before validating.

Options

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:

EventWhen
[: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:

EventWhen
[: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