The constructors. Every one of them returns a plain term.
alias Rupa.T
T.string(min: 1)
#=> {:string, [min: 1]}There is no macro here and no state anywhere: T.string/1 is a function that builds a
tuple, and you are free to write the tuple yourself. What the constructors add is canonical
form — a bare map becomes an object, and options come back sorted by key, so two spellings
of the same schema are the same term and hash the same under :erlang.phash2/1.
Nothing here checks anything. Rupa.Schema.validate/1 is the judge, and it reports every
problem in the tree at once.
The vocabulary
Scalars carry only options: string/1, integer/1, float/1, boolean/1, null/1.
Everything else carries a payload as well: literal/2, enum/2, object/2, list/2,
map_of/2, tuple/2, optional/2, nullable/2, ref/2, union/2, tagged/3.
Options every kind takes: default:, from:, to:. The rest are per kind and listed on
each function.
Required, optional, nullable
A field is required unless it is wrapped in optional/2 or carries default:. optional/2
is about the key, nullable/2 is about the value, and they compose. See
Rupa.Schema for the whole rule.
Names on the wire
from: and to: rename one field; rename_all: and keys: on object/2 do it wholesale.
All four belong to the object field, not to the type, so they go on the term the object's
field map holds — see Rupa.Schema for the rule and what each one means.
Summary
Functions
A boolean.
A string in format: :date, which decodes to a Date.
A string in format: :date_time, RFC 3339's, which decodes to a DateTime in UTC.
A string in format: :duration, which decodes to a Duration.
A validated email address, which stays a string.
A closed set of values — JSON Schema's enum.
A float. Options: gte:, gt:, lte:, lt:, multiple_of:.
A validated hostname, which stays a string.
An integer. Options: gte:, gt:, lte:, lt:, multiple_of:.
A validated IPv4 address, which stays a string.
A validated IPv6 address, which stays a string.
A list of one element type. Options: min:, max:, unique:.
One exact value — JSON Schema's const. Strings, atoms, numbers and booleans.
An object of unknown keys and one value type — JSON Schema's additionalProperties.
Merges two objects. The right-hand side wins on both fields and options.
JSON null, and nothing else. For "this value may be null", reach for nullable/2.
The value may be JSON null, which decodes to nil. Says nothing about the key.
An object, from a map of field name to schema. Options: rename_all:, unknown:, keys:,
into:, defs:.
Drops the named fields of an object.
The key may be absent. Only meaningful on an object field.
Makes every field of an object optional, one level deep.
Keeps only the named fields of an object.
A named reference — JSON Schema's $ref. :root is "#"; any other name resolves against
defs:.
A string. Options: min:, max:, len:, pattern:, format:.
A tagged union — JSON Schema's oneOf with a discriminator.
A string in format: :time, RFC 3339's full-time, which decodes to a Time in UTC. The
offset is part of the format: "08:30:00+07:00" decodes to ~T[01:30:00], and "08:30:00"
is an error.
A fixed-length list, one schema per position — JSON Schema's prefixItems.
An untagged union — JSON Schema's anyOf. Opt in with tag: :none.
A validated URI, which stays a string.
A validated UUID, which stays a string.
Types
@type t() :: Rupa.Schema.t()
Functions
A boolean.
iex> Rupa.T.boolean()
{:boolean, []}
A string in format: :date, which decodes to a Date.
iex> Rupa.T.date()
{:string, [format: :date]}
A string in format: :date_time, RFC 3339's, which decodes to a DateTime in UTC.
iex> Rupa.T.datetime()
{:string, [format: :date_time]}
A string in format: :duration, which decodes to a Duration.
iex> Rupa.T.duration()
{:string, [format: :duration]}
A validated email address, which stays a string.
iex> Rupa.T.email()
{:string, [format: :email]}
A closed set of values — JSON Schema's enum.
iex> Rupa.T.enum([:admin, :member], default: :member)
{:enum, [:admin, :member], [default: :member]}
A float. Options: gte:, gt:, lte:, lt:, multiple_of:.
iex> Rupa.T.float(gt: 0)
{:float, [gt: 0]}
A validated hostname, which stays a string.
iex> Rupa.T.hostname()
{:string, [format: :hostname]}
An integer. Options: gte:, gt:, lte:, lt:, multiple_of:.
iex> Rupa.T.integer(gte: 0, lte: 150)
{:integer, [gte: 0, lte: 150]}
A validated IPv4 address, which stays a string.
iex> Rupa.T.ipv4()
{:string, [format: :ipv4]}
A validated IPv6 address, which stays a string.
iex> Rupa.T.ipv6()
{:string, [format: :ipv6]}
A list of one element type. Options: min:, max:, unique:.
unique: is checked on the decoded elements, not the wire: two entries that differ only where
decoding discards — a stripped unknown key, an integer that decodes like a float — count as the
same value, so the decoded list round-trips.
iex> Rupa.T.list(Rupa.T.integer(), max: 5)
{:list, {:integer, []}, [max: 5]}
One exact value — JSON Schema's const. Strings, atoms, numbers and booleans.
iex> Rupa.T.literal("v2")
{:literal, "v2", []}
An object of unknown keys and one value type — JSON Schema's additionalProperties.
iex> Rupa.T.map_of(Rupa.T.integer())
{:map_of, {:integer, []}, []}
Merges two objects. The right-hand side wins on both fields and options.
defs: entries are merged rather than replaced, and two entries of the same name with
different schemas raise.
iex> Rupa.T.merge(%{a: Rupa.T.string()}, %{b: Rupa.T.integer()})
{:object, %{a: {:string, []}, b: {:integer, []}}, []}
JSON null, and nothing else. For "this value may be null", reach for nullable/2.
iex> Rupa.T.null()
{:null, []}
The value may be JSON null, which decodes to nil. Says nothing about the key.
iex> Rupa.T.nullable(Rupa.T.string())
{:nullable, {:string, []}, []}
An object, from a map of field name to schema. Options: rename_all:, unknown:, keys:,
into:, defs:.
A bare map is already an object, so object/1 matters only when you want the options.
rename_all: spells every field's wire key in one style, keys: chooses whether the decoded
map is keyed by atoms or strings, and unknown: says what to do with a wire key the schema
does not name — :strip, :error or :keep.
Field names are atoms, or strings for an object whose names you did not choose — see
Rupa.Schema for the rule and for what keys: then defaults to.
into: names a struct module to decode into, and it is an option on this object rather
than on the compile, so an object nested three levels down becomes a struct the same way the
root does. Rupa.Schema has what it cannot hold with, and mix rupa.gen.struct writes the
modules.
iex> Rupa.T.object(%{id: Rupa.T.uuid()}, unknown: :error)
{:object, %{id: {:string, [format: :uuid]}}, [unknown: :error]}
iex> Rupa.T.object(%{"id" => Rupa.T.uuid()})
{:object, %{"id" => {:string, [format: :uuid]}}, []}
iex> Rupa.T.object(%{first_name: Rupa.T.string()}, rename_all: :camelCase)
{:object, %{first_name: {:string, []}}, [rename_all: :camelCase]}
iex> Rupa.T.object(%{name: Rupa.T.string()}, into: URI)
{:object, %{name: {:string, []}}, [into: URI]}
iex> Rupa.T.object(%{name: Rupa.T.string()})
{:object, %{name: {:string, []}}, []}
Drops the named fields of an object.
iex> user = %{id: Rupa.T.uuid(), email: Rupa.T.email()}
iex> Rupa.T.omit(user, [:email])
{:object, %{id: {:string, [format: :uuid]}}, []}
The key may be absent. Only meaningful on an object field.
Absent decodes to the key not being there, not to nil. Pair it with default: on the
optional itself to fill the gap instead.
iex> Rupa.T.optional(Rupa.T.string())
{:optional, {:string, []}, []}
Makes every field of an object optional, one level deep.
A field that already carries default: is left alone: it was never required.
iex> Rupa.T.partial(%{name: Rupa.T.string(), age: Rupa.T.integer(default: 0)})
{:object, %{name: {:optional, {:string, []}, []}, age: {:integer, [default: 0]}}, []}
Keeps only the named fields of an object.
These four are honest map work on the field map, so they rewrite the root object: a
ref(:root) anywhere inside now points at the rewritten object, not the original.
iex> user = %{id: Rupa.T.uuid(), name: Rupa.T.string(), email: Rupa.T.email()}
iex> Rupa.T.pick(user, [:id])
{:object, %{id: {:string, [format: :uuid]}}, []}
A named reference — JSON Schema's $ref. :root is "#"; any other name resolves against
defs:.
iex> Rupa.T.ref(:root)
{:ref, :root, []}
A string. Options: min:, max:, len:, pattern:, format:.
pattern: is PCRE source in a string, compiled once when the schema is compiled. format:
is one of Rupa.Schema.formats/0. The four formats that decode to a value -- :date_time,
:date, :time, :duration -- re-encode to a canonical spelling the incoming one need not
match, so a length or pattern check cannot hold beside them and is refused at compile time;
the six that hand the string back unchanged take one.
iex> Rupa.T.string(min: 1)
{:string, [min: 1]}
A tagged union — JSON Schema's oneOf with a discriminator.
tag is the field holding the branch name. Without content: the branches are internally
tagged, so each one is an object that carries the tag itself; with content: they are
adjacently tagged, serde style, and a branch can be any schema.
Either way it decodes to {tag, value} — {:circle, %{r: 1.0}} — with the tag interned
from the branch name at compile time, so a decoded value is one case away from handled.
iex> Rupa.T.tagged(:type, %{"circle" => %{r: Rupa.T.float()}})
{:tagged, %{"circle" => {:object, %{r: {:float, []}}, []}}, [tag: :type]}
A string in format: :time, RFC 3339's full-time, which decodes to a Time in UTC. The
offset is part of the format: "08:30:00+07:00" decodes to ~T[01:30:00], and "08:30:00"
is an error.
iex> Rupa.T.time()
{:string, [format: :time]}
A fixed-length list, one schema per position — JSON Schema's prefixItems.
iex> Rupa.T.tuple([Rupa.T.float(), Rupa.T.float()])
{:tuple, [{:float, []}, {:float, []}], []}
An untagged union — JSON Schema's anyOf. Opt in with tag: :none.
It costs one decode attempt per variant, in the order you wrote them, and the first that
takes the value wins. Nesting multiplies that, so Rupa makes you say you meant it — and warns
when you nest one anyway. tagged/3 is the one that stays flat.
Because the first that fits wins, a variant an earlier one already accepts is never reached:
union([T.float(), T.integer()], tag: :none) decodes every number as a float. Rupa does not
check for that, and on the module backend the compiler may say so in its own words, about a
function in the generated codec rather than about the schema.
Encoding costs one decode too. A decoded value does not record which branch it came from, so to
stay a true inverse the encoder writes each branch in turn and keeps the first whose wire decodes
back to the value — otherwise a branch that merely accepts the value's type could drop a field a
later branch kept. A value no branch round-trips is a :no_variant error, the same as decoding
one nothing accepts.
iex> Rupa.T.union([Rupa.T.integer(), Rupa.T.string()], tag: :none)
{:union, [{:integer, []}, {:string, []}], [tag: :none]}
A validated URI, which stays a string.
iex> Rupa.T.uri()
{:string, [format: :uri]}
A validated UUID, which stays a string.
iex> Rupa.T.uuid()
{:string, [format: :uuid]}