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
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub.
Docs are at OpenaiEx User Gu...
New
WebAuthnLiveComponent WebAuthnComponents
See this post about renaming the package.
Passwordless authentication for Phoenix LiveView app...
New
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
Castle is a package I put together to assist with building Elixir releases capable of being live-upgraded via the ‘official’ release hand...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New
Other Trending Topics
A little off-topic, but I feel like people here have a good head on their shoulders.
I used to be quite good at making software. Was luc...
New
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with.
But I don’t use it anymore.
Serverless was the promis...
New
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly)
AI dream has many faces, but general...
New
What IDE or editor are you using for Elixir development?
Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
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
- #ai
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixirconf-us
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #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).