Is there a better way to generate OpenAPI schemas for Phoenix REST APIs?

Hey everyone,

In Django, we have tools like drf-spectacular that can automatically generate OpenAPI schemas from serializers without extra work. I’m wondering if there’s something similar for Phoenix?

Right now, I’m using open_api_spex, but it feels a bit tedious to manually declare the request and response schemas for each endpoint, even if the paths get picked up automatically. Is there a better workflow or library for this in the Phoenix ecosystem? Or do people usually just accept the manual declarations as part of the process?

Any tips or experiences would be appreciated!

1 Like

Would you like to derive the schemas from something like typespecs or something?

OpenApiSpex makes you write the schemas but you get the deserializers from it. Serializers are generally just a @derive attribute.

I released oaskit recently and I’m all ears if you have ideas or examples to make the whole process less tedious.

1 Like

I believe the only fully-automated option is to use Ash Framework with AshJsonApi. But that also means you must be OK with using JSON:API, since that is what gets generated.

@zachdaniel, anything to add here?

1 Like

The pattern we use could be applied to non JSON:API APIs as well, but we don’t focus on that with ash_json_api too much. But with generic actions you can write any API endpoint shape you want.

2 Likes

Thanks for sharing oaskit. I see that the JSV library you use allows importing an existing schema from JSON. Can I then use that for oaskit (given the schema will contain a full API rather than just one type)?

I still believe it’s better to generate the code from the schema instead of the opposite.

2 Likes

I would agree, and with GraphQL too.

I wonder if a tool like Igniter is the way to achieve this given that most Elixir libraries are code-first.

For GraphQL it’s quite simple: Absinthe.Schema.Notation — absinthe v1.7.10

1 Like

Yes but not from a JSV schema directly. If you want to use an OpenAPI spec from a file you should follow this guide (it’s on a branch as I’m adding features but it’s already supported on the released version of the lib).

2 Likes

Yeah, ideally you declare the shape of the request and response using structs that you are already using in your code, again, like django serializers, and the schema gets derived from there.

Oaskit looks cool, thank you.

1 Like

Modules using JSV defschema are structs:

defmodule Pet do
  import JSV

  defschema %{
    type: :object,
    properties: %{
      name: %{type: :string, default: "keke"},
      kind: JSV.Schema.string_to_atom_enum([:dog, :cat])
    }
  }
end

root = JSV.build!(Pet)
JSV.validate!(%{"name" => "piglet", "kind" => "dog"}, root) |> dbg()
# => %Pet{kind: :dog, name: "piglet"}

Now there is an undocumented defschema_for macro that lets you write a schema that will cast to another struct on validation. For instance defining a schema for an Ecto schema module. So on validation (with casts enabled) you get a struct from that ecto schema as the output. It’s nice but I’m not documenting it because the changeset would also need to be run most of the time. So I’d rather let users define their own cast functions (with defcast) to do as they please. I’m sure a new “ecto to jsv” package could do just that but I’m not in need right now.

Json schemas are way more powerful with all the anyOf, if/then, $ref and even $dynamicRef. Honestly I’d rather write a solid JSON schema and only put stuff related to foreign keys and unique constraints in the changesets. But my team is not there yet.

Oaskit looks cool, thank you.

Thank you :slight_smile: Full disclaimer, it’s almost the same as OpenApiSpex.

2 Likes

leaving a comment to support this somehow.

Been writing that code for 3 weeks now, over and over again…

1 Like

I did a spike a while back to generate an OpenAPI schema from our Phoenix Conn Tests of a very large and complicated JSON API: GitHub - zblanco/swole: Generates OpenAPI based specs or docs from Phoenix/Plug ConnTests.

It’s made to be part of CI/CD to publish the schema as part of docs where the source of truth happens to be the controller tests.

It’s a niche - perhaps temporary use case if you want to document an existing API and manually creating your OpenAPI schema is an arduous task. I’d recommend moving to using the actual schema as the source of truth instead of tests & other code as with a solution such as: GitHub - E-xyza/Exonerate: JSONSchema -> Elixir code generator.

Piping the JSON schema output into an LLM to simplify it into components may work if the context window is large enough.

2 Likes

I think I’ll update JSV to not expect a schema/0 function but rather a json_schema/0 function, which will look more specialized, and could be directly provided in an Ecto module:

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  use JSV.Schema

  @primary_key {:id, :binary_id, autogenerate: true}
  schema "users" do
    field :name, :string
    field :age, :integer
    belongs_to :organization, MyApp.Accounts.Organization

    timestamps()
  end


  def json_schema do
    %{
      type: :object,
      properties: %{
        name: string(),
        age: integer()
      },
      required: [:name, :age]
    }
    |> with_cast(__MODULE__, :from_json)
  end

  defcast from_json(attrs) do
    {:ok, changeset(attrs)}
  end

def changeset(...), do: #...
end

It’s still verbose though.

Using the tests is interesting because you only document what’s really tested, and so what is really supported. But too convoluted to my taste. For instance if a parameter or body field accepts string or integer you need to somehow merge the schemas, and also ignore requests that verify that the backend returns an error, etc.

bureaucrat does something similar

1 Like

Yeah I wrote Swole to replace our usage of Bureaucrat due to some issues. It’s more or less a ground up rewrite taking the same approach of hooking into test runs but with functional separation of concerns so that the same %Swole.APISpec{} struct can be used to encode into different formats (json, markdown, slate, blueprint, etc).

It made sense in our case for internal documentation between developer teams for a large existing JSON API with extensive tests.

1 Like

I created docout partially because I didn’t like the DX of open_api_spex but it certainly doesn’t solve the problem of needing to define schemas for each endpoint somewhere. I’m curious how that would work for anything but the simplest API designs (all fields in serialization logic are accepted and returned 1 for 1)…

1 Like