andreashasse
Spectral - Type-driven JSON encoding/decoding, validation, and OpenAPI generation
I’m happy to announce Spectral, a library that lets your Elixir structs and @type specs become the single source of truth for validation, encoding/decoding (primarily JSON), and OpenAPI schema generation. If you’re familiar with Pydantic in the Python world, the idea is similar.
Who is this for?
Spectral is aimed at developers building and consuming JSON who want to avoid keeping multiple representations of the same information in sync — a type definition here, validation logic there, a JSON schema somewhere else. If your types already express the shape of your data, Spectral lets them do more of the work.
Example
defmodule Person do
defstruct [:name, :age, :role]
@type role :: :user | :admin
@type t :: %Person{
name: String.t(),
age: non_neg_integer(),
role: role()
}
@spec from_json(binary()) :: {:ok, t()} | {:error, [Spectral.Error.t()]}
def from_json(json), do: Spectral.decode(json, __MODULE__, :t, :json)
@spec to_json(t()) :: {:ok, iodata()} | {:error, [Spectral.Error.t()]}
def to_json(person), do: Spectral.encode(person, __MODULE__, :t, :json)
end
{:ok, person} = Person.from_json(~s({"name": "Alice", "age": 30, "role": "admin"}))
#=> {:ok, %Person{name: "Alice", age: 30, role: :admin}}
Person.to_json(person)
#=> {:ok, ...}
Person.from_json(~s({"name": "Alice", "age": -1, "role": "admin"}))
#=> {:error, [%Spectral.Error{location: ["age"], type: :type_mismatch, ...}]}
# Generate OpenAPI schema
Spectral.schema(Person, :t)
Hex: spectral | Hex
Docs: Spectral v0.13.0 — Documentation
Most Liked Responses
DaAnalyst
Maybe combine the struct type and defstruct into one (with a macro)?
It’s been a while since I developed my deftypestruct and I’ve seen someone posting a library doing something very similar here like a week or two ago.
ex (my lib creates {module_name}.t(), but can be made to use a different type name too):
defmodule Person do
deftypestruct %{
name: String.t(),
age: non_neg_integer() | nil,
role: role()
}
end
I also check against nil so unless explicitly permitted (like the age field above) the lib raises.
mudasobwa
You might be interested in taking a look at estructura allowing transparent nesting, coercion, validation, and (!) generation for stream_data property-based testing out of the box.
andreashasse
The deftypestruct approach is elegant. As your library generates the type and put it in the beam it works well with spectral ![]()
Thanks for pointing out the nil handling in spectral, you can find more info in the spectral docs nil section.








