andreashasse
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
Trending in Announcing
Hey everyone!
Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application.
This library uses Erlang esaml to provide
plug enabl...
New
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries.
offset-based pagination with...
New
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
Hobbes is a low-level distributed database for the Elixir programming language.
Hobbes provides a simple, safe, and scalable storage lay...
New
Hi there! We created Gust: A task orchestrator inspired by Airflow.
For those who have never heard about Aiflow, it’s a Python-based wor...
New
Hi all!
I want to present a small library which provides a mix task for generating an Entity-Relationship Diagram for Ecto schemas.
You...
New
Other Trending Topics
I am happy to introduce the very α version of the new programming language compiled to BEAM.
Welcome Cure.
It has literally three kille...
New
This showed up on my feed.. anyone heard of it? Just hype?
Ox Alpha is a reasoning model designed for coding, sustained ag...
New
It’s not that it’s vocabulary is too advanced. It’s something worse.
I get lost trying to follow even a paragraph written by Claude. It’...
New
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
@hugobarauna, Dr. Dimitrios Koutmos (my brother) and I (Alex Koutmos) have been hard at work on writing a book on how you can use Elixir ...
New
Introductory paragraph
I’ll be looking for a keen junior or someone that has a couple of years experience in the real world (so you’ve be...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #blog-post
- #ai
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
DaAnalyst
Maybe combine the struct type and
defstructinto one (with a macro)?It’s been a while since I developed my
deftypestructand 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):I also check against
nilso unless explicitly permitted (like theagefield above) the lib raises.mudasobwa
You might be interested in taking a look at
estructuraallowing transparent nesting, coercion, validation, and (!) generation forstream_dataproperty-based testing out of the box.andreashasse
The
deftypestructapproach is elegant. As your library generates the type and put it in the beam it works well with spectralThanks for pointing out the nil handling in spectral, you can find more info in the spectral docs nil section.
Asd
Hi, good library, I’ve read the code and it has a lot of very strange approaches and things
It uses a
spectraerlang library, which uses caching in persistent_term for type-specs. But why do you need to introduce lazy-loading of the type-information (which would certainly introduce random spikes in runtime performance), when you can just create an encoder/decoder for a specific type during compilation? Just introduce a macro.Library says
but it also provides features of OpenAPI spec generations, including endpoint documentation, etc. So its not JSON codec generation, it is a code-first OpenAPI integration.
If you’re doing OpenAPI, there is a problem that OpenAPI JSON schema is not “compatible” with the elixir type specs. I mean that JSON Schema can define types which can’t be expressed in elixir type specs. And I don’t see any way to integrate JSON Schema definitions into elixir type specs.
It uses
spectra, which encodes record tuples as maps and there is no option to specify codecs for generic tuples as far as I can see. I tried naive approach with custom codec for:erlang.tuple/0type and it didn’t work.It fails to work in iex, because it uses
abstract_codechunk received with:code.get_object_code, which returns only for modules which are present in a code path. There must be an option to accept module binaryCode cache is not invalidated on recompilation, which would make development with this library a hell
I found some more issues, so feel free to contact me so I can perform a more detailed review (for a reasonable price of course)
andreashasse
Wow, had a quick look at estructura, nice work! Worth noting is that spectral doesn’t do coercion of integers that are encoded as strings in json. Eg, for the type:
@type my_int :: integer()Spectral will return error for the value “1”, but will work for 1. There is currently no option to do such coercion, but it can be added.
{:error, …} = Spectral.decode(~s(“1”), MyModule, :my_int, :json){:ok, 1} = Spectral.decode(~s(1), MyModule, :my_int, :json)Internally in spectra (the erlang library that does most of the heavy lifting), there is some support for property-based testing, but it is not ready for general use.
Apart from this I think we are aligned in features?
mudasobwa
More or less, yep. I am not sure if spectral works with nested structs as in Estructura.User — estructura v1.13.0
Ah, and estructura allows calculated fields.
DaAnalyst
It looks nice, but does it generate typespecs under the hood (for those types declared there with atoms)?
mudasobwa
It’s on my todo-list since the day 1, but I never needed it myself and therefore this not-so-complicated mapping is not yet there. ATM, it does generate stubs.
andreashasse
Hi Asd,
I started out gathering up all type information at compile time (as you suggest), but when you reference types from modules that don’t use the Spectral macro, you still need to figure out the types from those modules somehow. The cache (which is turned off by default, so it shouldn’t have bothered you when developing) is documented here: Configuration If you are doing hot code reloading in production, then I can expose primitives for cleaning the cache.
In the documentation there is an example that defines a “point type”, which is a 2-tuple. Imho, there is no good general way to convert tuples to JSON, when you have such a type you have to create your own codec: Custom codecs. My hope is that you should be able to create elixir types that map to most things OpenAPI has support for. With the codec “escape hatch” you should be able to do whatever you want
It is correct that the types currently need to be expressed in a beam file that is compiled with debug_info, this is a current limitation of the library. If this is hindering developers from using the library, then I think it can be solved.
I’m happy to hear about those other issues.
All the best
Asd
Yes, exactly, you can call the
ensure_compiled?on the module and then extract the types. It is possible that module is not compiled and cyclic references are possible too, that’s why the best approach would be to use the compiler or a compile tracer for the problem.I’d solve it like this: compile tracer collects info about every type spec definition, and then generates a module like
Spectral.Generated.<Application name>which exposes a functionget_type_info(Module) -> TypeInfo. Then functions likeencodeanddecodejust use this module.But it is all hacks. Metaprogramming in Elixir and Erlang is purposefully limited to block any attempts to change the compilation of one module, based on the contents of another, aside function calls and macros.
Dont get me wrong, but one
get_object_codeis a bunch of disk IO operations. And one type info gathering may be result in multipleget_object_codecalls, because one type can reference another and so on. Now imagine that your code now is guaranteed to take 2 seconds to just encode the response, at least once (with type info caching) or every time (without caching).