ExOpenApiUtils (ex_open_api_utils v0.17.1)
OpenAPI schema generation from Ecto schemas with OpenAPI 3.2 support.
Migration Guide
From v0.9.x to v0.10.x (OpenAPI 3.2)
1. Update OpenAPI Version
In your ApiSpec module, update the version:
# Before (v0.9.x)
%OpenApi{
openapi: "3.0.0",
...
}
# After (v0.10.x)
%OpenApi{
openapi: ExOpenApiUtils.openapi_version(), # Returns "3.2.0"
...
}2. Migrate Tags (Optional - for tag hierarchy)
If using flat tags, no changes needed. For hierarchical tags:
# Before (v0.9.x) - flat tags
%OpenApi{
tags: [
%OpenApiSpex.Tag{name: "Users"},
%OpenApiSpex.Tag{name: "Profile"},
%OpenApiSpex.Tag{name: "Admin"}
]
}
# After (v0.10.x) - hierarchical tags with 3.2 fields
alias ExOpenApiUtils.Tag
%OpenApi{
tags: [
Tag.new("Users", summary: "User Management"),
Tag.nested("Profile", "Users", summary: "User Profiles"),
Tag.navigation("Admin", summary: "Admin Panel")
] |> Tag.to_open_api_spex_list()
}3. Remove Deprecated Extensions (if using Redoc-specific)
Replace non-standard extensions with OpenAPI 3.2 native fields:
| Old (Redoc) | New (3.2 native) |
|---|---|
x-tagGroups | Use Tag.nested/3 |
x-displayName | Use summary field |
Extensions Retained
These extensions are kept for TypeScript/NestJS codegen compatibility:
x-enum-varnames- TypeScript enum member namesx-order- Property ordering in generated code
Basic Usage
Define schemas with use ExOpenApiUtils:
defmodule MyApp.User do
use ExOpenApiUtils
open_api_property(
key: :name,
schema: %Schema{type: :string, description: "User name"}
)
@primary_key {:id, :binary_id, autogenerate: true}
schema "users" do
field :name, :string
end
open_api_schema(
title: "User",
description: "Application user",
required: [:name],
properties: [:name],
tags: ["Users"]
)
endPolymorphic embeds
ex_open_api_utils bridges polymorphic_embed's Ecto side to OpenApiSpex's
oneOf + discriminator composition. Declare the bridge with a single
open_api_polymorphic_property/1 call alongside the matching
polymorphic_embeds_one:
defmodule MyApp.Notification do
use ExOpenApiUtils
open_api_property(key: :subject, schema: %Schema{type: :string})
open_api_polymorphic_property(
key: :channel,
type_field_name: :__type__,
open_api_discriminator_property: "channel_type",
variants: [
email: EmailChannel,
sms: SmsChannel,
webhook: WebhookChannel
]
)
schema "notifications" do
field :subject, :string
polymorphic_embeds_one :channel,
types: [email: EmailChannel, sms: SmsChannel, webhook: WebhookChannel],
type_field_name: :__type__,
on_type_not_found: :raise,
on_replace: :update
end
open_api_schema(title: "Notification", ...)
endThe library generates one parent-contextual variant submodule per
(parent, variant, direction) triple at the parent's __before_compile__
time via Module.create with an allOf composition body. The generated
siblings (e.g. NotificationEmailChannelRequest /
NotificationEmailChannelResponse) carry the discriminator as a real
defstruct field, so Kernel.struct/2 preserves it through the full
cast pipeline — closing GH-30, where the pre-fix variant submodule's
defstruct was built without the discriminator and silently dropped it
at Cast.Object.to_struct/1.
See open_api_polymorphic_property/1 for the full option list.
Summary
Functions
Declares a polymorphic field whose underlying Ecto schema is a
polymorphic_embeds_one and whose OpenAPI representation is a oneOf + discriminator composition. A single call replaces the previous
paired-open_api_property + polymorphic_embed_discriminator shape.
Returns the OpenAPI version string for 3.2 compliance.
Alias for Tag module for convenience.
Functions
Declares a polymorphic field whose underlying Ecto schema is a
polymorphic_embeds_one and whose OpenAPI representation is a oneOf + discriminator composition. A single call replaces the previous
paired-open_api_property + polymorphic_embed_discriminator shape.
The library generates one parent-contextual variant submodule per
(parent, variant, direction) triple at the parent's __before_compile__
time, composing each new sibling via
allOf: [<original variant submodule>, <inline discriminator patch>] so
the generated defstruct includes the discriminator field as a real atom
key. The parent's own oneOf + discriminator.mapping is synthesised to
point at the new siblings, so the round-trip cast preserves the
discriminator through Kernel.struct/2.
Options
:key(required) — atom. The field name on the parent that is both apolymorphic_embeds_oneand the name of the synthesised writeOnly/readOnly OpenAPI properties.:type_field_name(required) — atom. Must match thetype_field_name:option passed topolymorphic_embeds_one. This is the atom key thatcast_polymorphic_embed/3reads from the flattened params map; it may differ from the wire discriminator name.:open_api_discriminator_property(required) — non-empty string. The wire discriminator key that will appear in the JSON body and that OpenApiSpex'sCast.Discriminatorwill read to route the variant.:variants(required) — non-empty keyword list. Each entry iswire_atom: EctoVariantModule. The wire value written to and read from the JSON body isAtom.to_string(wire_atom), and the Ecto variant module must be one of the modules listed in the matchingpolymorphic_embeds_one's:typesoption. Each variant module must itselfuse ExOpenApiUtilsand have at least oneopen_api_schema/1declaration so the library can reflect on its auto-generated Request/Response submodules.
Example
open_api_polymorphic_property(
key: :channel,
type_field_name: :__type__,
open_api_discriminator_property: "channel_type",
variants: [
email: EmailChannel,
sms: SmsChannel,
webhook: WebhookChannel
]
)
schema "notifications" do
field :subject, :string
polymorphic_embeds_one :channel,
types: [
email: EmailChannel,
sms: SmsChannel,
webhook: WebhookChannel
],
type_field_name: :__type__,
on_type_not_found: :raise,
on_replace: :update
end
@spec openapi_version() :: String.t()
Returns the OpenAPI version string for 3.2 compliance.
Alias for Tag module for convenience.