libo
Elixact - schema definition and validation (think Pydantic in Elixir)
Hey,
I have used a bunch of schema libraries in Elixir and didn’t find the one satisfies all my needs. At some point, I kinda miss Python for having Pydantic. So recently I started to make one for myself.
Elixact
Elixact is a schema definition and validation library, inspired by Python’s Pydantic.
Features
Intuitive Schema DSL - Similar to Ecto.Schemasyntax
Strong Type Validation - Comprehensive validation for basic and complex types
JSON Schema Support - Automatic generation of JSON Schema from your Elixir schemas
Custom Types - Easily define reusable custom types
Nested Schemas - Support for deeply nested data structures
Field Constraints - Rich set of built-in constraints
Structured Errors - Clear and actionable error messages
See the doc for more details: elixact v0.1.2 — Documentation
Github: GitHub - LiboShen/elixact: schema definition and validation library for Elixir
Hex: elixact | Hex
Credits / Prior Arts
As I mentioned above, I have tried many libraries to give me strong data validations. I took inspirations from all of them:
Ecto is the obvious choice when you need simple a schema.
Typestruct is a lightweight improvement of the builtin struct.
Drops is the closest of what I need. But I failed to make it work with nested or complex schemas.
I’m using it for my several projects now. Let me know if you find this is useful. I’m open for collaboration to make it better. Thanks.
Most Liked
Eiji
Here are my few cents:
gteq and lteq is not a common naming. I recommend ge and le respectively.
gt and others looks good as a short key in options Keyword, but too short for DSL. I would recommend to add between, count_between (for array), length_between (for string) and maybe even change naming for example from gt to greater_than. If you do not like it you can alternatively create a constraints macro.
defmodule Example do
use Elixact
schema "name" do
field :age, :integer do
constraints gt: 0, lt: 10
end
end
end
field :settings, {:map, {:string, {:union, [:string, :boolean, :integer]}}} do
This is not bad for a contributors in your project, but it doesn’t look best as DSL. You can consider using @spec notation, so you can convert it’s AST into such data structure within your library.
field :settings, %{String.t() => String.t() | boolean | integer}) do
looks much more clear.
{block, _opts} =
Keyword.pop(
opts,
:do,
quote do
end
)
You can change the default to {:__block__, [], []}, so it would look like:
{block, _opts} = Keyword.pop(opts, :do, {:__block__, [], []})
Since you don’t use _opts you can change it to:
block = opts[:do] || {:__block__, [], []}
and since you are not using opts in other place you can also change the function heading to:
defmacro field(name, type, opts \\ [do: {:__block__, [], []}])
defmacro field(name, type, do: block) do
That’s -7 lines of code (LOC) without losing any feature. ![]()
var!(field_meta) =
As you may know variable field_meta would not be hygienized. I would recommend to store it in other ways. A common practice is to use module attributes, but those as same as variables may conflict with the code. What I personally like is to store data in process. People don’t use Process often and especially in compile time. If you would use your own Agent (with moduledoc set to false) which starts only in compile time then it would be even better as there is no chance that some developer would use such data.
Many modules are not documented. Consider adding documentation or use @moduledoc false in case some module is not part of public API. To avoid such things in future I recommend to give a try credo tool.
Elixact.Application is starting without any children. If you don’t plan to use it you don’t have to even create such file. All you have to do is to remove mod: {Elixact.Application, []} in application function inside Elixact.MixProject.
The file lib/elixact/config.ex is empty. If you don’t use it you should remove it.
sodapopcan
I have yet to use one of these libraries—I just keep using Ecto for now even though it gets clunky for general use—but I really like this DSL. Nice work, I’ll keep it in mind. Also, good name ![]()
libo
Hey, these tips are super helpful.
- I like the
constraintsmacro approach. It provides a natural grouping for value constraints and other field metadata. - Using
@specnotation is nicer, I agree. Never thought about it before. I’d like to look into it. - And thanks for the compile time
Agenttip. It’s kinda eye-opening TBH. - I’ll do housekeeping for the silly leftovers.
mudasobwa
Last week I shamelessly drop a link to my estructura library for the second time in a row, but still: @spec notation would make one obliged to implement an ad hoc, informally-specified, bug-ridden, slow implementation of half of dialyzer. One cannot allow spec notation and then restrict it to some types. Working with the remote complex types is a pain and a ton of code.
That’s why I went with StreamData types, what literally granted me the no-code implementation of data generation for property-based testing.
D4no0
It seems that there is a huge flow of type validation libraries lately and all try to define their own DSL or data structure for validation.
Folk that is already doing these kind of validations for years at the edge of the system are using ecto + embedded schemas. Ecto strikes the perfect balance between compile-time definition of base types and runtime validation with changesets.
It has some shortcomings including:
- Too tied to database - type validation code, custom types are very tied to database and it has inherently some bugs that could be reworked entirely;
- No composition, only associations - this can be easily solved even in base ecto with some metaprogramming, but it would be nice to have native support;
- Schemas are tied to a module - this one is hard to decide, however I would prefer if schemas were not tied to a module.
Solve all of these problems while keeping the ecto way of doing things and you will not only will have a library that works well and it’s very flexible, but everyone that already uses ecto for non-database validations will happily migrate over, me including.







