Is it possible to reuse Ecto schema instead of defining new schema in Open API Spex?

Hi all, in my phoenix project I have an Ecto schema and simple controller for providing REST API. Now I want to provide swagger API for third-party users of this API. According to Open API Spex documents I should define schema for each request parameters and responses. The REST API is just for fetching some instance and creating new instance. Is it possible to reuse my Instance schema instead of defining new schema for Open API Spex?

Here is the Instance schema and the related controller:

# instance.ex
defmodule ZYR.Sys.Instance do
  use Ecto.Schema
  import Ecto.Changeset

  @derive {Jason.Encoder, except: [:__meta__]}

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "instance" do
    field(:code, :string)
    field(:family, :string)
    field(:name, :string)

    timestamps()
  end

  @doc false
  def changeset(instance, attrs) do
    instance
    |> cast(attrs, [:name, :code, :family])
    |> validate_required([:name, :code, :family])
  end
end

def index(conn, _params) do
  render(conn, "index.json", %{instances: Sys.list_instances()})
end

Yes, it is possible to define a schema for Open API Spex using reflection.

Thanks a lot, you means something like this?

 defmodule InstanceResponse do
    @behaviour OpenApiSpex.Schema
    @derive [Jason.Encoder]
    @schema %Schema{
      ZYR.Sys.Instance.__schema__(:fields)
    }
 end

Exactly this code won’t compile.
You can start from this snippet

defmodule MyOpenApiSchemaBuilder do
  defmacro defschema(module) do
    quote bind_quoted: [module: module] do
      require OpenApiSpex

      OpenApiSpex.schema(%{
        title: inspect(module),
        type: :object,
        properties:
          module.__schema__(:fields)
          |> Map.new(fn field_name ->
            type = Ecto.Type.type(module.__schema__(:type, field_name))
            {field_name, %OpenApiSpex.Schema{type: type}}
          end)
      })
    end
  end
end

defmodule MyUserSchema do
  import MyOpenApiSchemaBuilder
  defschema(MyApp.User)
end
3 Likes

I’m certain that it’s possible, but I’d argue it’s not a good idea.

An API used by third-parties is a commitment; changing it will require coordination. Tightly coupling that to the underlying schema through generated code means that now the database schema can’t easily change either.

1 Like

I’ve done something like this and I think it’s preferable to maintaining a separate, very similar list of attributes/types. It seems to me that coupling is desired in this case. If I’ve removed a column I’ve documented in my API I don’t want the documentation to stay the same.

I do think at least there needs to be a way to specify which db fields should be added to the schema, since not all are relevant. Maybe something like that is what you were thinking of when you said “tightly” coupled?