Open-source project
open-api-spex/open_api_spex avatar
open-api-spex/open_api_spex

Open API Spex: an OpenAPI 3 spec generated from your Plug router

Open API Specifications for Elixir Plug applications

839 stars216 forksElixirMPL-2.0

At a glance

What is it?
Open API Spex documents, validates and tests Plug and Phoenix APIs from one OpenAPI 3 spec built in Elixir code. The spec is a struct you assemble, not a hand written YAML file.
Who is it for?
Open API Spex suits teams already on Plug or Phoenix that want documentation, param validation and response checks to come from the same source, and it suits anyone tired of maintaining a static OpenAPI file. It does not suit a project that cannot add a compile time dependency or that needs the raw camelCase field names used by the OpenAPI and JSON Schema conventions.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 54 days ago.
What is it written in?
Mainly Elixir, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One spec drives documentation, param casting and response tests

The pitch of Open API Spex is that one artifact serves several jobs. The README lists five uses: generating and serving a JSON Open API spec document from your code, using the spec to cast request params into well defined schema structs, validating params so bad requests stop before they reach your controllers, validating responses against schemas in tests, and exploring the API interactively with SwaggerUI. That last point matters more than it sounds. Because the document is produced from the same modules your controllers use, there is no second copy of the endpoint list to fall out of date. The trade-off is that the spec becomes build time code, so a project that cannot run Elixir code at build time has to look elsewhere.

The ApiSpec module is where the router becomes a document

Everything starts with an ApiSpec module that populates an OpenApiSpex.OpenApi struct. The README's example pulls servers from a Phoenix endpoint and paths from a Phoenix router, so the routes and their path parameters are read out of the application rather than declared twice. The struct is then passed through OpenApiSpex.resolve_schema_modules, which discovers request and response schemas from the path specs. If your application already carries a title and version in its OTP application spec, the README shows a second way to fill the info block with to_string calls on Application.spec. A result is a plain Elixir struct, which means you can inspect it in IEx, assert on it in a test, or reshape it before it is served.

Adding the dependency and importing the formatter

Installation is a mix.exs entry, and the README pins it to a minor version series so the library can move within 3.x. Add :open_api_spex to the deps list:

elixir
def deps do
  [
    {:open_api_spex, "~> 3.21"}
  ]
end

There is a second, smaller installation step that is easy to miss. The Elixir formatter would otherwise add parentheses around the arguments of the ControllerSpecs macro call, which changes how your controller reads. The fix is to list the library in import_deps inside .formatter.exs:

elixir
[
  import_deps: [:open_api_spex]
]

Without that line, the formatter quietly rewrites the macro calls the next time anyone runs mix format. Full documentation for the library lives on HexDocs, and the README points there for anything the page itself leaves out.

Operations are declared per controller action and can be omitted

For every plug or controller that handles API requests, an operation has to say what that action accepts and returns. The README's example controller uses OpenApiSpex.ControllerSpecs and then describes an update action with a summary, a path parameter, a request body and a response body, each tied to a schema module:

elixir
  operation :update,
    summary: "Update user",
    parameters: [
      id: [in: :path, description: "User ID", type: :integer, example: 1001]
    ],
    request_body: {"User params", "application/json", UserParams},
    responses: [
      ok: {"User response", "application/json", UserResponse}
    ]

Every such definition is converted into an OpenApiSpex.Operation struct. There is an escape hatch for actions you would rather leave out of the document, which is to pass false as the second argument of operation/2:

elixir
operation :create, false

That single line is worth remembering, because otherwise every action in a controller is forced into the spec.

Schemas are modules, and x-struct is an opt in

Request and response bodies are described by schema modules that call OpenApiSpex.schema/1 with a definition whose keys follow OpenApiSpex.Schema.t. The macro produces a Schema struct that is exposed through a generated schema/0 function. The title key is optional and otherwise derived from the last segment of the module name. The part that will affect your code generation is the x-struct option: adding "x-struct": __MODULE__ to a schema asks the library to turn the described data into a struct linked to the JSON schema. Leave it out and you keep plain maps, add it and your incoming params arrive as typed structs. The README also points to the %Operation{} struct as a second style for cases where the ControllerSpecs macros do not give enough flexibility, with a full example controller in the examples directory of the repository.

Definitions are cached and the field names are not camelCase

Two details affect how the library behaves in production. First, the operation definitions are read by the application's ApiSpec module, which is called from the OpenApiSpex.Plug.PutApiSpex plug on each request, but the definition data is cached, so it is not extracted again on every request. The spec is therefore built once and reused, which is what makes it cheap enough to serve in production. Second, the field names follow snake_case rather than the camelCase convention used by OpenAPI and JSON Schema, so request_body and security_schemes appear where the specification writes requestBody and securitySchemes. Any tooling that consumes the generated document, from code generators to your own clients, has to cope with that difference.

Editorial conclusion

Open API Spex suits teams already on Plug or Phoenix that want documentation, param validation and response checks to come from the same source, and it suits anyone tired of maintaining a static OpenAPI file. It does not suit a project that cannot add a compile time dependency or that needs the raw camelCase field names used by the OpenAPI and JSON Schema conventions. Before adopting, confirm that your router and endpoint can be handed to Paths.from_router and Server.from_endpoint, since those two calls are the entry point the rest of the library depends on.

Frequently asked questions

How do I add Open API Spex to an Elixir project?

Add :open_api_spex to the deps list in mix.exs, using the version constraint the README shows. The library is published on Hex with documentation on HexDocs.

How do I document a Phoenix controller action with Open API Spex?

Use OpenApiSpex.ControllerSpecs in the controller and describe the action with the operation macro, giving it a summary, parameters, a request_body and responses. Each definition is turned into an OpenApiSpex.Operation struct and read by your ApiSpec module.

Does Open API Spex work with plain Plug applications, not only Phoenix?

Yes. The project targets Plug and Phoenix APIs, and the repository contains both examples/phoenix_app/ and examples/plug_app/. The spec is read through the OpenApiSpex.Plug.PutApiSpex plug on each request, with the definition data cached.

Why do the generated OpenAPI fields use snake_case names?

The README states that the names of the OpenAPI fields follow the snake_case naming convention instead of the camelCase convention used by OpenAPI and JSON Schema, so fields appear as request_body rather than requestBody.

Can I skip documenting an action in Open API Spex?

Yes. Passing false as the second argument of operation/2 omits that action from the spec, which is useful for actions you do not want published.

Official sources

  1. Issues
  2. License: MPL-2.0
  3. open-api-spex/open_api_spex on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/open-api-spex-open-api-spex.svg)](https://hysenlabs.com/projects/open-api-spex-open-api-spex)

Was this useful?