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?
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.
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.
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)?
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).
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.
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 Full disclaimer, it’s almost the same as OpenApiSpex.
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.
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
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.
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.
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)…