andreashasse

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)

:package: Hex: spectral | Hex
:open_book: Docs: Spectral v0.13.0 — Documentation

https://github.com/andreashasse/spectral

Showing Posts 1 to 10

DaAnalyst

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

mudasobwa

Creator of Cure

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

andreashasse OP

The deftypestruct approach is elegant. As your library generates the type and put it in the beam it works well with spectral :slight_smile:

Thanks for pointing out the nil handling in spectral, you can find more info in the spectral docs nil section.

Asd

Asd

Hi, good library, I’ve read the code and it has a lot of very strange approaches and things

  • It uses a spectra erlang 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

    Spectral provides type-safe data serialization and deserialization for Elixir types. Currently the focus is on JSON.

    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/0 type and it didn’t work.

  • It fails to work in iex, because it uses abstract_code chunk 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 binary

  • Code 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

andreashasse OP

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

mudasobwa

Creator of Cure

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

DaAnalyst

It looks nice, but does it generate typespecs under the hood (for those types declared there with atoms)?

mudasobwa

mudasobwa

Creator of Cure

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

andreashasse OP

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 :wink:

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

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 function get_type_info(Module) -> TypeInfo. Then functions like encode and decode just 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_code is a bunch of disk IO operations. And one type info gathering may be result in multiple get_object_code calls, 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).

Where Next? Top

Trending in Announcing Top

bluzky
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...
387 15136 120
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 11030 135
New
JesseHerrick
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
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
woylie
Phoenix components for pagination, sortable tables and filter forms with Flop and (optionally) Ecto. pagination cursor pagination sorta...
New

Other Trending Topics Top

akoutmos
@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
mudasobwa
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
alexslade
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
bartblast
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
Herve37
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
lawik
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

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews