Schemaless Ecto changesets with support for nesting and JSON Schemas.
Usage
Define fields and validation functions for your data structures:
defmodule Example do
def changeset(params) do
fields = [
%{name: :name, type: :string, title: "Full Name"},
%{name: :email, type: :string, description: "User email address"},
%{name: :address, type: Schemecto.one(
[
%{name: :street, type: :string},
%{name: :city, type: :string},
%{name: :zip, type: :string}
],
with: &Example.validate_address/1
)}
]
Schemecto.new(fields, params)
|> Ecto.Changeset.validate_required([:name, :email])
|> Ecto.Changeset.validate_format(:email, ~r/@/)
end
def validate_address(changeset) do
changeset
|> Ecto.Changeset.validate_required([:street, :city])
|> Ecto.Changeset.validate_length(:zip, is: 5)
end
endSince Schemecto is built on Ecto changesets, you can use all standard Ecto validation functions. The field list can be constructed dynamically at runtime, making it ideal for scenarios where the schema must be composed at runtime. Each field definition supports metadata like title, description, deprecated, and default values.
JSON Schema Generation
Convert your changesets to JSON Schema to generate API documentation, validate client-side forms, or integrate with other tools:
Schemecto.to_json_schema(changeset)The generated schema includes field metadata, required fields, format patterns, length constraints, numeric bounds, and enum values from your changeset. For instance, the changeset above will emit:
{
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"city": {
"type": "string"
},
"street": {
"type": "string"
},
"zip": {
"type": "string",
"maxLength": 5,
"minLength": 5
}
},
"required": ["street", "city"]
},
"email": {
"type": "string",
"description": "User email address",
"pattern": "@"
},
"name": {
"type": "string",
"title": "Full name"
}
},
"required": ["name", "email"]
}Supported types
| Ecto type | JSON type |
|---|---|
:integer | integer |
:float | number |
:decimal | number |
:boolean | boolean |
:string | string |
:map | object |
{:array, type} | array of type |
{:array, :any} | array of object |
Ecto.Enum of `type | enum of type |
Custom Ecto types and parameterized types are also supported as long as they emit one of the types above. More types can be added in the future too.
Schemecto.one/2 and Schemecto.many/2 should be preferred instead of
:map when the fields are known upfront.
Summary
Functions
Defines a nested validation for cardinality :many.
Creates a new schemaless changeset with the given field definitions.
Defines a nested validation for cardinality one.
Converts a changeset's types into a JSON schema.
Functions
Defines a nested validation for cardinality :many.
Parameters
fields- List of field definitions for each nested changesetopts- Keyword list of options::with- A 2-arity function that receives a changeset and params, and returns a validated changeset (required)
Examples
def validate_tag(changeset) do
changeset
|> Ecto.Changeset.validate_required([:name])
end
fields = [
%{name: :title, type: :string},
%{name: :tags, type: Schemecto.many(
[
%{name: :name, type: :string},
%{name: :color, type: :string}
],
with: &validate_tag/1
)}
]
changeset = Schemecto.new(fields, params)
Creates a new schemaless changeset with the given field definitions.
If parameters are given, they are cast into the changeset according to fields. Parameters are a keyword list, a map of string or atom keys, or nil.
Parameters
fields- List of field definitions. Each field is a map with::name- Field name (required):type- Field type (required):description- Human-readable description (optional):title- Human-readable title (optional):deprecated- Boolean indicating if field is deprecated (optional):default- Default value for the field (optional)
Examples
fields = [
%{name: :name, type: :string, title: "Full Name"},
%{name: :age, type: :integer, default: 0, description: "Age in years"}
]
params = %{"name" => "John", "age" => 30}
changeset = Schemecto.new(fields, params)
Defines a nested validation for cardinality one.
Parameters
fields- List of field definitions for the nested changesetopts- Keyword list of options::with- A 1-arity function that receives a changeset with the parameters already cast into them (if any) (required)
Examples
def validate_address(changeset) do
changeset
|> Ecto.Changeset.validate_required([:street, :city])
end
fields = [
%{name: :name, type: :string},
%{name: :email, type: :string},
%{name: :address, type: Schemecto.one(
[
%{name: :street, type: :string},
%{name: :city, type: :string},
%{name: :zip, type: :string}
],
with: &validate_address/1
)}
]
changeset = Schemecto.new(fields, params)
Converts a changeset's types into a JSON schema.
Takes a changeset and returns a JSON schema based on the changeset's metadata.
Note the "$schema" property is not included in the schema for easier embedding, but it is recommended to be set to "https://json-schema.org/draft/2020-12/schema".
Examples
iex> fields = [
...> %{name: :name, type: :string, title: "Full Name"},
...> %{name: :age, type: :integer}
...> ]
iex> changeset = Schemecto.new(fields)
iex> Schemecto.to_json_schema(changeset)
%{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string", "title" => "Full Name"},
"age" => %{"type" => "integer"}
}
}