ExOpenApiUtils
OpenAPI 3.2 schema generation from Ecto schemas for Elixir/Phoenix applications.
Features
- OpenAPI 3.2 compliant - Native support for tag hierarchy (
parent,kind,summary) - Ecto schema integration - Define OpenAPI schemas alongside your Ecto schemas
- Auto-generated Request/Response schemas - Separate schemas for input (writeOnly) and output (readOnly)
- Property ordering -
x-orderextension for consistent code generation - TypeScript codegen support -
x-enum-varnamesfor proper TypeScript enum generation
Installation
Add ex_open_api_utils to your dependencies in mix.exs:
def deps do
[
{:ex_open_api_utils, "~> 0.10.0"}
]
endQuick 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"]
)
endThis generates:
MyApp.OpenApiSchema.UserRequest- For input (excludesreadOnlyfields)MyApp.OpenApiSchema.UserResponse- For output (excludeswriteOnlyfields)
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
endBest 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:
required: [:field]- Field must be present in the payloadnullable: true- Field can have a null valuerequired: [:field]+nullable: true- Field must be present but can be null-
Not in required +
nullable: true- Field can be omitted or explicitly set to null
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: navigationMigration 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-tagGroups | Tag.nested/3 |
x-displayName | summary field |
4. Removed helpers
The following helpers were removed in v0.10.0 to focus on standard OpenAPI compliance:
with_constraints/2- Use standard OpenAPI fields (minLength,maxLength,pattern, etc.)with_transforms/2- Handle transforms in your application layerwith_relation/2,belongs_to/2,has_many/2,has_one/2- Use$reffor related schemaswith_pagination/2- Define pagination schemas explicitlywith_db_hints/2- Database hints are not part of OpenAPI specwith_metadata/2,internal_schema/1,deprecated_schema/2- Use standarddeprecatedfield
Extensions retained
These extensions are kept for TypeScript/NestJS code generation:
x-enum-varnames- TypeScript enum member names (viaHelpers.enum_schema/1)x-order- Property ordering in generated code (auto-generated)
API Reference
ExOpenApiUtils
openapi_version/0- Returns"3.2.0"tag/2- Creates a tag (delegates toTag.new/2)nested_tag/3- Creates a nested tag (delegates toTag.nested/3)navigation_tag/2- Creates a navigation tag (delegates toTag.navigation/2)
ExOpenApiUtils.Tag
new(name, opts)- Create a basic tagnested(name, parent, opts)- Create a tag nested under a parentnavigation(name, opts)- Create a navigation tagto_open_api_spex(tag)- Convert to OpenApiSpex.Tagto_open_api_spex_list(tags)- Convert list of tags
ExOpenApiUtils.Helpers
enum_schema(opts)- Create enum schema withx-enum-varnames
Documentation
Full documentation is available at HexDocs.
License
MIT License - see LICENSE.md