Elixir typespec syntax → JSON Schema, at compile time.
Write familiar Elixir types and get a JSON Schema map with zero runtime cost. See the overview for a full guide with examples.
Type mapping
| Elixir type | JSON Schema |
|---|---|
String.t() | {"type": "string"} |
binary() | {"type": "string"} |
integer() | {"type": "integer"} |
pos_integer() | {"type": "integer", "minimum": 1} |
non_neg_integer() | {"type": "integer", "minimum": 0} |
neg_integer() | {"type": "integer", "maximum": -1} |
float() | {"type": "number"} |
number() | {"type": "number"} |
boolean() | {"type": "boolean"} |
map() | {"type": "object"} |
atom() | {"type": "string"} |
term() / any() | {} (no constraints) |
:a | :b | :c | {"type": "string", "enum": ["a", "b", "c"]} |
[String.t()] | {"type": "array", "items": {"type": "string"}} |
%{key: type} | nested object |
optional(:key) => type | omitted from required |
type | nil | omitted from required |
Summary
Functions
Converts a string-keyed map to an atom-keyed map using the schema's
"properties" as the source of allowed keys.
Generates JSON Schema from normalized field/type metadata.
Converts an Elixir typespec AST to a JSON Schema map at compile time.
Types
@type schema_field() :: %{ :name => String.t(), :type => schema_type(), optional(:required) => boolean() }
@type schema_type() :: atom() | {:literal, term()} | {:enum, [atom()]} | {:nullable, schema_type()} | {:one_of, [schema_type()]} | {:list, schema_type()} | {:map, :string, schema_type()} | {:object, [schema_field()]} | {:ref, term()} | {:schema, map() | boolean()}
A normalized type description for schema generation.
Functions
Converts a string-keyed map to an atom-keyed map using the schema's
"properties" as the source of allowed keys.
Unknown keys are left as strings. Enum values are converted to atoms. Nested objects and arrays of objects are atomized recursively.
Examples
iex> import JSONSpec
iex> my_schema = schema(%{required(:name) => String.t(), optional(:age) => integer()})
iex> JSONSpec.atomize(my_schema, %{"name" => "Alice", "age" => 30})
%{name: "Alice", age: 30}Enum string values become atoms:
iex> import JSONSpec
iex> my_schema = schema(%{status: :active | :inactive})
iex> JSONSpec.atomize(my_schema, %{"status" => "active"})
%{status: :active}Nested objects are atomized recursively:
iex> import JSONSpec
iex> my_schema = schema(%{user: %{name: String.t(), role: :admin | :member}})
iex> JSONSpec.atomize(my_schema, %{"user" => %{"name" => "Alice", "role" => "admin"}})
%{user: %{name: "Alice", role: :admin}}Arrays of objects too:
iex> import JSONSpec
iex> my_schema = schema(%{items: [%{id: integer(), status: :on | :off}]})
iex> JSONSpec.atomize(my_schema, %{"items" => [%{"id" => 1, "status" => "on"}]})
%{items: [%{id: 1, status: :on}]}
@spec from_type( schema_type(), keyword() ) :: map() | boolean()
Generates JSON Schema from normalized field/type metadata.
Unlike schema/2, this function accepts data rather than quoted Elixir syntax.
Fields use their JSON names and explicit requiredness; omitted :required
means optional. This lets codec libraries apply aliases and defaults without
duplicating JSON Schema generation.
JSONSpec.from_type({:object, [
%{name: "name", type: :string, required: true},
%{name: "age", type: {:nullable, :non_neg_integer}}
]}):resolve accepts a unary function returning a type description for a
{:ref, id}. Unrecognized atom types also resolve through this callback,
allowing consumers to resolve remote modules without JSONSpec depending on
those modules. Cycles become local $ref pointers; acyclic objects stay inline.
{:one_of, types} describes an Elixir union and emits anyOf, since its
alternatives may overlap. Nullable values use anyOf with a null branch by
default. Field optionality
is independent of nullability. nullable: :legacy emits the nonstandard
"nullable": true form for adapters that must preserve an existing contract.
The schema/2 macro's existing optional-field semantics are unchanged.
{:schema, value} passes through an already-built object or boolean schema
without rebasing its references. No cache is retained between calls.
Converts an Elixir typespec AST to a JSON Schema map at compile time.
The macro captures the quoted form of the type expression and converts it to a JSON Schema map. The result is a plain map embedded in your compiled code.
Options
:doc- Keyword list mapping field names to description strings
Examples
iex> import JSONSpec
iex> schema(%{name: String.t(), age: integer()})
%{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string"},
"age" => %{"type" => "integer"}
},
"required" => ["name", "age"],
"additionalProperties" => false
}
iex> import JSONSpec
iex> schema([String.t()])
%{"type" => "array", "items" => %{"type" => "string"}}
iex> import JSONSpec
iex> schema(:active | :inactive | :pending)
%{"type" => "string", "enum" => ["active", "inactive", "pending"]}With descriptions:
iex> import JSONSpec
iex> schema(
...> %{required(:name) => String.t(), optional(:age) => integer()},
...> doc: [name: "Full name", age: "Age in years"]
...> )
%{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string", "description" => "Full name"},
"age" => %{"type" => "integer", "description" => "Age in years"}
},
"required" => ["name"],
"additionalProperties" => false
}