Schemecto (Schemecto v0.1.0)

Copy Markdown View Source

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
end

Since 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 typeJSON type
:integerinteger
:floatnumber
:decimalnumber
:booleanboolean
:stringstring
:mapobject
{:array, type}array of type
{:array, :any}array of object
Ecto.Enum of `typeenum 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

many(fields, opts)

Defines a nested validation for cardinality :many.

Parameters

  • fields - List of field definitions for each nested changeset
  • opts - 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)

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)

one(fields, opts)

Defines a nested validation for cardinality one.

Parameters

  • fields - List of field definitions for the nested changeset
  • opts - 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)

to_json_schema(changeset)

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"}
  }
}