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-tagGroupsUse Tag.nested/3
x-displayNameUse summary field

Extensions Retained

These extensions are kept for TypeScript/NestJS codegen compatibility:

  • x-enum-varnames - TypeScript enum member names
  • x-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"]
  )
end

Polymorphic 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", ...)
end

The 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

is_readOnly?(module)

is_writeOnly?(module)

nested_tag(name, parent, opts \\ [])

See ExOpenApiUtils.Tag.nested/3.

open_api_polymorphic_property(opts)

(macro)

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 a polymorphic_embeds_one and the name of the synthesised writeOnly/readOnly OpenAPI properties.
  • :type_field_name (required) — atom. Must match the type_field_name: option passed to polymorphic_embeds_one. This is the atom key that cast_polymorphic_embed/3 reads 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's Cast.Discriminator will read to route the variant.
  • :variants (required) — non-empty keyword list. Each entry is wire_atom: EctoVariantModule. The wire value written to and read from the JSON body is Atom.to_string(wire_atom), and the Ecto variant module must be one of the modules listed in the matching polymorphic_embeds_one's :types option. Each variant module must itself use ExOpenApiUtils and have at least one open_api_schema/1 declaration 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

open_api_property(opts)

(macro)

open_api_schema(opts)

(macro)

openapi_version()

@spec openapi_version() :: String.t()

Returns the OpenAPI version string for 3.2 compliance.

tag(name, opts \\ [])

Alias for Tag module for convenience.