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
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
New
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries.
offset-based pagination with...
New
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub.
Docs are at OpenaiEx User Gu...
New
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly.
One of i...
New
Phoenix components for pagination, sortable tables and filter forms with Flop and (optionally) Ecto.
pagination
cursor pagination
sorta...
New
Please say hi to a new lib, Astro that aims to deliver easy-to-consume astronomy calculations of practical use. For now it only calculat...
New
Other Trending Topics
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
I am seeing a lot of aplications of Argumentum ad Vericundiam in software discussions. They do link some piece of writing and point us to...
New
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog
It says that Fly is going all-in on sprites, which is a worry ...
New
Hey folks,
I just published a post about Hologram’s funding and where the project goes next - the short version:
Curiosum as Main Spons...
New
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using.
We’re particularly inte...
New
I was thinking since Goatmire Elixir turned out pretty good I should maybe do another one. 30th of Sep - 2nd of Oct this year./
The firs...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #ai
- #iex
- #graphql
- #elixirconf-us
- #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).