Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.1] - 2026-06-17

### Changed

- Documentation now surfaces Spectral's type features directly: a "Going further with Spectral" reference table in the README (string/length/pattern constraints, field aliases, `only`, struct defaults, enums, built-in and custom codecs), hexdocs links throughout, and "powered by Spectral" pointers in the module docs. The example app gained a `type_parameters` string-constraint demonstration with a test.

## [0.6.0] - 2026-06-14

### Added
Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# PhoenixSpectral

PhoenixSpectral integrates [Spectral](https://github.com/andreashasse/spectral) with Phoenix, making controller typespecs the single source of truth for OpenAPI 3.1 spec generation and request/response validation. Define your types once — PhoenixSpectral derives the API docs and enforces them at runtime.
PhoenixSpectral integrates [Spectral](https://hexdocs.pm/spectral) with Phoenix, making controller typespecs the single source of truth for OpenAPI 3.1 spec generation and request/response validation. Define your types once — PhoenixSpectral derives the API docs and enforces them at runtime.

> **Most of the power lives in Spectral.** PhoenixSpectral is a thin Phoenix adapter; the type system that shapes and validates your requests and responses is [Spectral](https://hexdocs.pm/spectral). Features like string/length/pattern constraints, camelCase field aliases, custom codecs, and the built-in date/time codecs are configured on your *types* via Spectral, not here. Read the [Spectral docs](https://hexdocs.pm/spectral) and the [Going further with Spectral](#going-further-with-spectral) section below before assuming a capability is missing.

## Installation

Expand All @@ -9,7 +11,7 @@ Add `phoenix_spectral` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:phoenix_spectral, "~> 0.6.0"}
{:phoenix_spectral, "~> 0.6.1"}
]
end
```
Expand All @@ -18,7 +20,7 @@ end

### Step 1: Define typed structs with Spectral

[Spectral](https://github.com/andreashasse/spectral) is an Elixir library that validates, decodes, and encodes data according to your `@type` definitions. Add `use Spectral` to a module and your types become the schema — PhoenixSpectral reads them to validate requests, decode inputs, encode responses, and generate the OpenAPI spec.
[Spectral](https://hexdocs.pm/spectral) is an Elixir library that validates, decodes, and encodes data according to your `@type` definitions. Add `use Spectral` to a module and your types become the schema — PhoenixSpectral reads them to validate requests, decode inputs, encode responses, and generate the OpenAPI spec.

```elixir
defmodule MyApp.User do
Expand Down Expand Up @@ -261,7 +263,7 @@ make integration-test # runs the ExUnit suite in-process

## Configuration

PhoenixSpectral delegates encoding, decoding, and schema generation to [Spectral](https://github.com/andreashasse/spectral) / [spectra](https://github.com/andreashasse/spectra). Configure them directly in `config/config.exs` (or `config/runtime.exs`).
PhoenixSpectral delegates encoding, decoding, and schema generation to [Spectral](https://hexdocs.pm/spectral) / [spectra](https://hexdocs.pm/spectra). Configure them directly in `config/config.exs` (or `config/runtime.exs`).

### Custom codecs

Expand All @@ -276,7 +278,7 @@ config :spectra, :codecs, %{
}
```

The key is `{ModuleOwningType, {:type, type_name, arity}}`. User-configured codecs always take precedence over built-ins. See the [Spectral codec guide](https://github.com/andreashasse/spectral) for writing your own codecs with `use Spectral.Codec`.
The key is `{ModuleOwningType, {:type, type_name, arity}}`. User-configured codecs always take precedence over built-ins. See the [Spectral codec guide](https://hexdocs.pm/spectral/readme.html#custom-codecs) for writing your own codecs with `use Spectral.Codec`.

### Production: enable the module types cache

Expand All @@ -297,6 +299,38 @@ spectra skips Unicode validation of list-based strings by default. Enable it whe
config :spectra, :check_unicode, true
```

## Going further with Spectral

PhoenixSpectral only wires Phoenix to Spectral — it adds no validation or schema features of its own. Everything below is a **Spectral** feature you configure on your *types*; PhoenixSpectral then applies it automatically to request decoding, response encoding, and the generated OpenAPI spec. This list is a map, not the full manual — follow the links into the [Spectral docs](https://hexdocs.pm/spectral) for the details.

| Want to… | Use Spectral's… | Docs |
|---|---|---|
| Constrain a string's length or shape (min/max length, regex `pattern`, `format`) **without writing a codec** | `spectral type_parameters: %{min_length: …, max_length: …, pattern: …}` on a `String.t()` type | [String and binary constraints](https://hexdocs.pm/spectral/readme.html#string-and-binary-constraints) |
| Expose `camelCase` (or any) JSON keys while keeping `snake_case` structs | `spectral field_aliases: %{first_name: "firstName"}` | [Field Aliases](https://hexdocs.pm/spectral/readme.html#field-aliases) |
| Hide internal fields (e.g. `password_hash`) or expose different views of one struct | `spectral only: [:id, :name]` | [Field Filtering with `only`](https://hexdocs.pm/spectral/readme.html#field-filtering-with-only) |
| Make a body field optional / supply a default | struct `defstruct` defaults + nullable types | [Struct defaults](https://hexdocs.pm/spectral/readme.html#struct-defaults) |
| Accept an enum from a path/query param (e.g. `?role=admin`) | an atom-union type `:: :admin \| :user`, decoded via the `binary_string` format | [Data Serialization API](https://hexdocs.pm/spectral/readme.html#data-serialization-api) |
| Serialize `DateTime`, `Date`, or `MapSet` | the built-in codecs (registered automatically) | [Built-in Codecs](https://hexdocs.pm/spectral/readme.html#built-in-codecs) |
| Encode/decode a domain type with custom rules (prefixed IDs, money, etc.) | `use Spectral.Codec` | [Custom Codecs](https://hexdocs.pm/spectral/readme.html#custom-codecs) |
| Add `title`, `description`, or example payloads to a schema | `spectral title:`, `description:`, `examples_function:` | [Documenting Types with `spectral`](https://hexdocs.pm/spectral/readme.html#documenting-types-with-spectral) |
| Annotate a path/header/query parameter's description | a named type alias with `spectral description: …` | [Parameter descriptions](#parameter-descriptions) (above) |

For example, length and pattern validation needs no controller code at all — declare the constraint on the type and PhoenixSpectral enforces it on every request and advertises it in the OpenAPI schema:

```elixir
defmodule MyApp.Types do
use Spectral

spectral type_parameters: %{min_length: 3, max_length: 30, pattern: "^[a-z0-9_]+$"}
@type username :: String.t()
end

# A request body field typed as username() now rejects "ab" or "Bad Name" with a 400,
# and the OpenAPI schema shows minLength/maxLength/pattern.
```

If you reach for `conn.body_params` or hand-roll validation in a controller, stop and check this table first — Spectral almost certainly does it declaratively.

## Design

- **Typespecs are the single source of truth** — no separate schema definitions; `@spec` drives both docs and validation
Expand Down
6 changes: 5 additions & 1 deletion example/lib/example/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,18 @@ defmodule Example.Types do
# the request body — a missing email field decodes as nil rather than an error.
defstruct [:name, email: nil]

# type_parameters enforces length constraints (no codec) and emits them into the schema.
spectral(type_parameters: %{min_length: 2, max_length: 50})
@type name :: String.t()

spectral(
title: "UserInput",
description: "Input for creating or updating a user. email is optional.",
examples_function: {__MODULE__, :examples, []}
)

@type t :: %UserInput{
name: String.t(),
name: name(),
email: String.t() | nil
}

Expand Down
9 changes: 9 additions & 0 deletions example/test/example_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ defmodule ExampleTest do

assert conn.status == 201
end

test "returns 400 when name is shorter than the min_length constraint" do
conn =
build_conn()
|> authed()
|> post("/users", Jason.encode!(%{name: "A"}))

assert conn.status == 400
end
end

describe "Bearer auth on write endpoints" do
Expand Down
5 changes: 5 additions & 0 deletions lib/phoenix_spectral.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ defmodule PhoenixSpectral do
Controllers that `use PhoenixSpectral.Controller` and define typespecs on their
action functions become the single source of truth for OpenAPI documentation.

The schema for each request and response is derived from your types by `Spectral`.
Schema details — descriptions, examples, string constraints, field aliases, custom
codecs — are declared on the types via Spectral's `spectral/1` macro; see the
[Spectral docs](https://hexdocs.pm/spectral).

## Usage

{:ok, spec} = PhoenixSpectral.generate_openapi(MyAppWeb.Router, %{title: "My API", version: "1.0.0"})
Expand Down
4 changes: 4 additions & 0 deletions lib/phoenix_spectral/controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ defmodule PhoenixSpectral.Controller do
Phoenix `(conn, params)`. Request data is decoded and validated against your typespecs,
and responses are encoded automatically.

Decoding, validation, and encoding are performed by `Spectral` from the types in
your `@spec`. See the [Spectral docs](https://hexdocs.pm/spectral) for how to shape
a type (optional fields, string constraints, field aliases, custom codecs).

## Usage

defmodule MyAppWeb.UserController do
Expand Down
7 changes: 5 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule PhoenixSpectral.MixProject do
def project do
[
app: :phoenix_spectral,
version: "0.6.0",
version: "0.6.1",
elixir: ">= 1.18.0",
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_paths(Mix.env()),
Expand All @@ -23,7 +23,10 @@ defmodule PhoenixSpectral.MixProject do
defp package do
[
licenses: ["Apache-2.0"],
links: %{"GitHub" => "https://github.com/andreashasse/phoenix_spectral"}
links: %{
"GitHub" => "https://github.com/andreashasse/phoenix_spectral",
"Spectral" => "https://hexdocs.pm/spectral"
}
Comment thread
andreashasse marked this conversation as resolved.
]
end

Expand Down
Loading