ExOpenApiUtils

OpenAPI 3.2 schema generation from Ecto schemas for Elixir/Phoenix applications.

CIModule VersionHex DocsTotal DownloadLicenseLast Updated

Features

Installation

Add ex_open_api_utils to your dependencies in mix.exs:

def deps do
  [
    {:ex_open_api_utils, "~> 0.10.0"}
  ]
end

Quick Start

1. Define your schema

defmodule MyApp.User do
  use ExOpenApiUtils

  open_api_property(
    key: :id,
    schema: %Schema{
      type: :string,
      format: :uuid,
      description: "User ID",
      readOnly: true
    }
  )

  open_api_property(
    key: :email,
    schema: %Schema{
      type: :string,
      format: :email,
      description: "User email address"
    }
  )

  open_api_property(
    key: :status,
    schema: Helpers.enum_schema(
      values: ["pending", "active", "suspended"],
      varnames: ["PENDING", "ACTIVE", "SUSPENDED"],
      description: "Account status"
    )
  )

  @primary_key {:id, :binary_id, autogenerate: true}
  schema "users" do
    field :email, :string
    field :status, :string
    timestamps()
  end

  open_api_schema(
    title: "User",
    description: "Application user",
    required: [:email],
    properties: [:id, :email, :status],
    tags: ["Users"]
  )
end

This generates:

2. Configure your API spec

defmodule MyApp.ApiSpec do
  alias OpenApiSpex.{Info, OpenApi, Server}
  alias ExOpenApiUtils.Tag

  @behaviour OpenApi

  @impl OpenApi
  def spec do
    %OpenApi{
      openapi: ExOpenApiUtils.openapi_version(),  # "3.2.0"
      info: %Info{
        title: "My API",
        version: "1.0.0"
      },
      servers: [%Server{url: "https://api.example.com"}],
      tags: tags()
    }
    |> OpenApiSpex.resolve_schema_modules()
  end

  defp tags do
    [
      Tag.new("Users", summary: "User management"),
      Tag.nested("Profile", "Users", summary: "User profiles"),
      Tag.navigation("Admin", summary: "Administration")
    ]
    |> Tag.to_open_api_spex_list()
  end
end

Best Practices

Use standard OpenAPI fields for validation

Standard OpenAPI schema fields are supported by all code generators:

open_api_property(
  key: :email,
  schema: %Schema{
    type: :string,
    format: :email,
    minLength: 5,
    maxLength: 255,
    pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
  }
)

Use readOnly and writeOnly appropriately

# Server-generated fields (not in request body)
open_api_property(
  key: :id,
  schema: %Schema{type: :string, format: :uuid, readOnly: true}
)

open_api_property(
  key: :created_at,
  schema: %Schema{type: :string, format: :"date-time", readOnly: true}
)

# Input-only fields (not in response)
open_api_property(
  key: :password,
  schema: %Schema{type: :string, minLength: 8, writeOnly: true}
)

Use nullable for optional fields

Mark fields as nullable when they can accept null values:

# Optional field (not in required list, can be omitted or null)
open_api_property(
  key: :middle_name,
  schema: %Schema{
    type: :string,
    nullable: true,
    description: "Optional middle name"
  }
)

# Required field that can be null (must be present, but can be null)
open_api_property(
  key: :nickname,
  schema: %Schema{
    type: :string,
    nullable: true,
    description: "Nickname (required field, but can be null)"
  }
)

# Nullable with readOnly (optional server-generated field)
open_api_property(
  key: :external_id,
  schema: %Schema{
    type: :string,
    format: :uuid,
    nullable: true,
    readOnly: true,
    description: "External system ID (may not be set yet)"
  }
)

# Nullable with writeOnly (optional input field)
open_api_property(
  key: :password,
  schema: %Schema{
    type: :string,
    minLength: 8,
    nullable: true,
    writeOnly: true,
    description: "Password (optional for updates)"
  }
)

Schema-level nullable:

Mark entire schemas as nullable in their definition:

# In your UIParameters module:
defmodule MyApp.UIParameters do
  use ExOpenApiUtils

  open_api_property(
    key: :theme,
    schema: %Schema{type: :string, description: "UI theme"}
  )

  schema "ui_parameters" do
    field :theme, :string
  end

  open_api_schema(
    title: "UIParameters",
    description: "UI configuration parameters",
    properties: [:theme],
    nullable: true  # The entire schema can be null
  )
end

# Then reference it simply:
open_api_property(
  schema: MyApp.OpenApiSchema.UIParametersResponse,
  key: :ui_parameters
)

Required vs Nullable:

Use enum_schema for TypeScript enums

open_api_property(
  key: :role,
  schema: Helpers.enum_schema(
    values: ["user", "admin", "moderator"],
    varnames: ["USER", "ADMIN", "MODERATOR"],
    description: "User role"
  )
)

Generated TypeScript:

export enum UserRole {
  USER = "user",
  ADMIN = "admin",
  MODERATOR = "moderator"
}

Use tag hierarchy for organized documentation

Tag.new("Settings", summary: "Application settings")
Tag.nested("Profile", "Settings", summary: "Profile settings")
Tag.nested("Security", "Settings", summary: "Security settings")
Tag.navigation("Admin", summary: "Admin panel")

Generated OpenAPI:

tags:
  - name: Settings
    summary: Application settings
  - name: Profile
    summary: Profile settings
    parent: Settings
  - name: Security
    summary: Security settings
    parent: Settings
  - name: Admin
    summary: Admin panel
    kind: navigation

Migration Guide

From v0.8.x/v0.9.x to v0.10.x

1. Update OpenAPI version

# Before
%OpenApi{openapi: "3.0.0", ...}

# After
%OpenApi{openapi: ExOpenApiUtils.openapi_version(), ...}  # Returns "3.2.0"

2. Migrate to tag hierarchy (optional)

# Before - flat tags
%OpenApi{
  tags: [
    %OpenApiSpex.Tag{name: "Users"},
    %OpenApiSpex.Tag{name: "Profile"}
  ]
}

# After - hierarchical tags
alias ExOpenApiUtils.Tag

%OpenApi{
  tags: [
    Tag.new("Users", summary: "User Management"),
    Tag.nested("Profile", "Users", summary: "User Profiles")
  ] |> Tag.to_open_api_spex_list()
}

3. Replace Redoc extensions with OpenAPI 3.2 native fields

Old (Redoc) New (OpenAPI 3.2)
x-tagGroupsTag.nested/3
x-displayNamesummary field

4. Removed helpers

The following helpers were removed in v0.10.0 to focus on standard OpenAPI compliance:

Extensions retained

These extensions are kept for TypeScript/NestJS code generation:

API Reference

ExOpenApiUtils

ExOpenApiUtils.Tag

ExOpenApiUtils.Helpers

Documentation

Full documentation is available at HexDocs.

License

MIT License - see LICENSE.md