Zodish (zodish v0.2.4)
View SourceZodish is a schema parser and validation library heavily inspired by JavaScript's Zod.
Summary
Types
Any Zodish type that has a shape (i.e. Zodish.Type.Map and
Zodish.Type.Struct).
Functions
Defines a type that accepts any value.
Defines an atom type.
Defines a boolean type.
Enables coercion for the given type.
Defines a date type.
Defines a date-time type.
Defines a decimal type.
Defines an email type (decorated String type).
Defines an enum type (atoms only).
Defines a float type.
Defines a integer type.
Updates the given type's :length option.
Defines a list type.
Defines a type that only accepts a specific value.
Defines a map type.
Updates the given type's :max option.
Merges two Map types into one, where :mode is inherited from the
most strict mode between the two given types.
Updates the given type's :min option.
Defines a number type.
Defines a numeric string type.
Removes the specified keys from the type's shape.
Makes a given inner type optional, where you can also define a
default value to be used when the actual value resolves to nil.
Checks whether the given type is an optional type.
Parses a value based on the given type.
Same as parse/1 but raises an error if failed to parse the given
params.
Makes all fields from the given type's shape optional (default nil).
Keeps only the specified keys from the type's shape.
Defines a record type.
Refines a value with a custom validation.
Switches the mode of the given schema to :strict, where additional fields are not allowed.
Defines a string type.
Switches the mode of the given schema to :strip, where additional fields are ignored.
Defines a struct type.
Infers the type spec of a given Zodish type to be used with @type.
Transforms the parsed value using a given function.
Defines a tuple type.
Defines a union type of 2 or more schemas.
Defines a string URI type.
Defines a UUID type (decorated String type).
Types
@type shaped() :: %{ :__struct__ => module(), :shape => %{required(atom()) => Zodish.Type.t()}, optional(atom()) => any() }
Any Zodish type that has a shape (i.e. Zodish.Type.Map and
Zodish.Type.Struct).
Functions
@spec any() :: Zodish.Type.Any.t()
Defines a type that accepts any value.
iex> Z.any()
iex> |> Z.parse("string")
{:ok, "string"}
iex> Z.any()
iex> |> Z.parse(123)
{:ok, 123}
iex> Z.any()
iex> |> Z.parse(%{key: "value"})
{:ok, %{key: "value"}}
iex> Z.any()
iex> |> Z.parse({:foo, :bar})
{:ok, {:foo, :bar}}
@spec atom(opts :: [option]) :: Zodish.Type.Atom.t() when option: {:coerce, boolean() | :unsafe}
Defines an atom type.
iex> Z.atom()
iex> |> Z.parse(:foo)
{:ok, :foo}Options
You can use :coerce to cast the given string into an atom.
iex> Z.atom(coerce: true)
iex> |> Z.parse("foo")
{:ok, :foo}By default, :coerce will only cast strings for existing atoms
since BEAM has a limited number of atom.
iex> Z.atom(coerce: true)
iex> |> Z.parse("alksdhfwejh")
{:error, %Zodish.Issue{message: "cannot coerce string \"alksdhfwejh\" into an existing atom"}}If you want to allow unsafe coercion of any string into an atom, you
can set :coerce to :unsafe.
iex> Z.atom(coerce: :unsafe)
iex> |> Z.parse("lskdjfalsdjf")
{:ok, :lskdjfalsdjf}
@spec boolean(opts :: [option]) :: Zodish.Type.Boolean.t() when option: {:coerce, boolean()}
Defines a boolean type.
iex> Z.boolean()
iex> |> Z.parse(true)
{:ok, true}Options
You can use :coerce to cast the given value into a boolean.
iex> Z.boolean(coerce: true)
iex> |> Z.parse("true")
{:ok, true}The accepted boolean-like values are:
| value | coerced to |
|---|---|
"true" | true |
"1" | true |
1 | true |
"yes" | true |
"y" | true |
"on" | true |
"enabled" | true |
"false" | false |
"0" | false |
0 | false |
"no" | false |
"n" | false |
"off" | false |
"disabled" | false |
@spec coerce(type, value :: boolean() | :unsafe) :: type when type: Zodish.Type.Atom.t()
@spec coerce(type, value :: boolean()) :: type when type: struct()
Enables coercion for the given type.
iex> Z.integer()
iex> |> Z.coerce()
iex> |> Z.parse("123")
{:ok, 123}If the given type doesn't support coercion but it encapsulates an inner type, then the coercion will be applied to the inner type.
iex> Z.optional(Z.integer())
%Zodish.Type.Optional{
inner_type: %Zodish.Type.Integer{coerce: false}
}
iex> Z.optional(Z.integer())
iex> |> Z.coerce()
%Zodish.Type.Optional{
inner_type: %Zodish.Type.Integer{coerce: true}
}
iex> Z.optional(Z.integer())
iex> |> Z.coerce()
iex> |> Z.parse("123")
{:ok, 123}
@spec date(opts :: [option]) :: Zodish.Type.Date.t() when option: {:coerce, boolean()}
Defines a date type.
iex> Z.date()
iex> |> Z.parse(~D[2025-06-27])
{:ok, ~D[2025-06-27]}Options
You can use :coerce to cast the given value into a Date.
iex> Z.date(coerce: true)
iex> |> Z.parse("2025-06-27")
{:ok, ~D[2025-06-27]}
@spec datetime(opts :: [option]) :: Zodish.Type.DateTime.t() when option: {:coerce, boolean()} | {:after, DateTime.t()} | {:after, mfa()} | {:after, (-> DateTime.t())} | {:after, {n :: integer(), unit :: :millisecond | :second | :minute | :hour | :day | :week | :month | :year, :from_now}} | {:before, DateTime.t()} | {:before, mfa()} | {:before, (-> DateTime.t())} | {:before, {n :: integer(), unit :: :millisecond | :second | :minute | :hour | :day | :week | :month | :year, :from_now}}
Defines a date-time type.
iex> Z.datetime()
iex> |> Z.parse(~U[2025-06-27T12:00:00.000Z])
{:ok, ~U[2025-06-27T12:00:00.000Z]}Options
You can use :coerce to cast the given value into a DateTime.
iex> Z.datetime(coerce: true)
iex> |> Z.parse("2025-06-27T12:00:00.000Z")
{:ok, ~U[2025-06-27T12:00:00.000Z]}You can use :after to ensure the given date-time is after a
certain timestamp.
iex> Z.datetime(after: ~U[2030-01-01T00:00:00.000Z])
iex> |> Z.parse(~U[2031-06-27T12:00:00.000Z])
{:ok, ~U[2031-06-27T12:00:00.000Z]}
iex> Z.datetime(after: ~U[2030-01-01T00:00:00.000Z])
iex> |> Z.parse(~U[2025-06-27T12:00:00.000Z])
{:error, %Zodish.Issue{message: "must be after 2030-01-01 00:00:00.000Z"}}Alternatively you can provide an MFA tuple or a function that
returns a DateTime:
iex> Z.datetime(after: {DateTime, :utc_now, []})
iex> |> Z.parse(~U[2030-01-01T00:00:00.000Z])
{:ok, ~U[2030-01-01T00:00:00.000Z]}
iex> Z.datetime(after: (fn -> DateTime.utc_now() end))
iex> |> Z.parse(~U[2030-01-01T00:00:00.000Z])
{:ok, ~U[2030-01-01T00:00:00.000Z]}You can also provide a relative time:
iex> {:ok, _} =
iex> Z.datetime(after: {15, :minute, :from_now})
iex> |> Z.parse(DateTime.add(DateTime.utc_now(), 16, :minute))
iex> {:error, _} =
iex> Z.datetime(after: {15, :minute, :from_now})
iex> |> Z.parse(DateTime.add(DateTime.utc_now(), 14, :minute))Likewise, you can use :before to ensure the given date-time is
before the given timestamp.
iex> {:ok, _} =
iex> Z.datetime(before: {15, :minute, :from_now})
iex> |> Z.parse(DateTime.add(DateTime.utc_now(), 14, :minute))
iex> {:error, _} =
iex> Z.datetime(before: {15, :minute, :from_now})
iex> |> Z.parse(DateTime.add(DateTime.utc_now(), 16, :minute))
@spec decimal(opts :: [option]) :: Zodish.Type.DateTime.t() when option: {:coerce, boolean()} | {:gt, Decimal.t()} | {:gt, Zodish.Option.t(Decimal.t())} | {:gte, Decimal.t()} | {:gte, Zodish.Option.t(Decimal.t())} | {:lt, Decimal.t()} | {:lt, Zodish.Option.t(Decimal.t())} | {:lte, Decimal.t()} | {:lte, Zodish.Option.t(Decimal.t())}
Defines a decimal type.
iex> Z.decimal()
iex> |> Z.parse(Decimal.from_float(3.14))
{:ok, Decimal.new("3.14")}Options
You can use :gt, :gte, :lt and :lte to constrain the allowed
values.
iex> Z.decimal(gt: Decimal.new("0.0"))
iex> |> Z.parse(Decimal.new("0.0"))
{:error, %Zodish.Issue{message: "expected a decimal greater than 0.0, got 0.0"}}
iex> Z.decimal(gte: Decimal.new("1.0"))
iex> |> Z.parse(Decimal.new("0.5"))
{:error, %Zodish.Issue{message: "expected a decimal greater than or equal to 1.0, got 0.5"}}
iex> Z.decimal(lt: Decimal.new("1.0"))
iex> |> Z.parse(Decimal.new("1.1"))
{:error, %Zodish.Issue{message: "expected a decimal less than 1.0, got 1.1"}}
iex> Z.decimal(lte: Decimal.new("1.0"))
iex> |> Z.parse(Decimal.new("1.1"))
{:error, %Zodish.Issue{message: "expected a decimal less than or equal to 1.0, got 1.1"}}You can use :coerce to cast the given value into a decimal before
validation.
iex> Z.decimal(coerce: true)
iex> |> Z.parse("3.14")
{:ok, Decimal.new("3.14")}
iex> Z.decimal(coerce: true)
iex> |> Z.parse("123")
{:ok, Decimal.new("123")}
@spec email(opts :: [option]) :: Zodish.Type.Email.t() when option: {:ruleset, :gmail | :html5 | :rfc5322 | :unicode}
Defines an email type (decorated String type).
iex> Z.email()
iex> |> Z.parse("foo@bar.com")
{:ok, "foo@bar.com"}Options
You can choose which ruleset to use for validating the email address
by setting the :ruleset option. There are 4 options available:
:gmail(default) - same rules as Gmail;:html5- same rules browsers use to validateinput[type=email]fields;:rfc5322- same rules as the classic emailregex.com (RFC 5322); and:unicode- a loose set of rules that allows Unicode (good for intl emails);iex> Z.email(ruleset: :gmail) iex> |> Z.parse("foo@bar.com") {:ok, "foo@bar.com"} iex> Z.email(ruleset: :html5) iex> |> Z.parse("foo@bar.com") {:ok, "foo@bar.com"} iex> Z.email(ruleset: :rfc5322) iex> |> Z.parse("foo@bar.com") {:ok, "foo@bar.com"} iex> Z.email(ruleset: :unicode) iex> |> Z.parse("foo@bar.com") {:ok, "foo@bar.com"}
Errors
When the given string is empty:
iex> Z.email()
iex> |> Z.parse("")
{:error, %Zodish.Issue{message: "cannot be empty"}}When the given string is not a valid email address:
iex> Z.email()
iex> |> Z.parse("foo@")
{:error, %Zodish.Issue{message: "invalid email address"}}
@spec enum(values :: [atom(), ...]) :: Zodish.Type.Enum.t()
@spec enum(opts :: [option]) :: Zodish.Type.Enum.t() when option: {:coerce, boolean()} | {:values, [atom(), ...]}
Defines an enum type (atoms only).
iex> Z.enum([:foo, :bar])
iex> |> Z.parse(:foo)
{:ok, :foo}Options
You can use :coerce to cast the given value into an atom.
iex> Z.enum(coerce: true, values: [:foo, :bar])
iex> |> Z.parse("bar")
{:ok, :bar}Errors
When the given value is not an atom:
iex> Z.enum([:foo, :bar])
iex> |> Z.parse("baz")
{:error, %Zodish.Issue{message: "expected an atom, got string"}}When the given value is not an atom but coerce is set to true:
iex> Z.enum(coerce: true, values: [:foo, :bar])
iex> |> Z.parse("baz")
{:error, %Zodish.Issue{message: "is invalid"}}
@spec float(opts :: [option]) :: Zodish.Type.Float.t() when option: {:coerce, boolean()} | {:gt, float()} | {:gt, Zodish.Option.t(float())} | {:gte, float()} | {:gte, Zodish.Option.t(float())} | {:lt, float()} | {:lt, Zodish.Option.t(float())} | {:lte, float()} | {:lte, Zodish.Option.t(float())}
Defines a float type.
iex> Z.float()
iex> |> Z.parse(3.14)
{:ok, 3.14}Options
You can use :gt, :gte, :lt and :lte to constrain the allowed
values.
iex> Z.float(gt: 0.0)
iex> |> Z.parse(0.0)
{:error, %Zodish.Issue{message: "expected a float greater than 0.0, got 0.0"}}
iex> Z.float(gte: 1.0)
iex> |> Z.parse(0.5)
{:error, %Zodish.Issue{message: "expected a float greater than or equal to 1.0, got 0.5"}}
iex> Z.float(lt: 1.0)
iex> |> Z.parse(1.1)
{:error, %Zodish.Issue{message: "expected a float less than 1.0, got 1.1"}}
iex> Z.float(lte: 1.0)
iex> |> Z.parse(1.1)
{:error, %Zodish.Issue{message: "expected a float less than or equal to 1.0, got 1.1"}}You can use :coerce to cast the given value into a float before
validation.
iex> Z.float(coerce: true)
iex> |> Z.parse("3.14")
{:ok, 3.14}
iex> Z.float(coerce: true)
iex> |> Z.parse("123")
{:ok, 123.0}
@spec integer(opts :: [option]) :: Zodish.Type.Integer.t() when option: {:coerce, boolean()} | {:gt, integer()} | {:gt, Zodish.Option.t(integer())} | {:gte, integer()} | {:gte, Zodish.Option.t(integer())} | {:lt, integer()} | {:lt, Zodish.Option.t(integer())} | {:lte, integer()} | {:lte, Zodish.Option.t(integer())}
Defines a integer type.
iex> Z.integer()
iex> |> Z.parse(3)
{:ok, 3}Options
You can use :gt, :gte, :lt and :lte to constrain the allowed
values.
iex> Z.integer(gt: 0)
iex> |> Z.parse(0)
{:error, %Zodish.Issue{message: "expected an integer greater than 0, got 0"}}
iex> Z.integer(gte: 0)
iex> |> Z.parse(-1)
{:error, %Zodish.Issue{message: "expected an integer greater than or equal to 0, got -1"}}
iex> Z.integer(lt: 1)
iex> |> Z.parse(2)
{:error, %Zodish.Issue{message: "expected an integer less than 1, got 2"}}
iex> Z.integer(lte: 1)
iex> |> Z.parse(2)
{:error, %Zodish.Issue{message: "expected an integer less than or equal to 1, got 2"}}You can use :coerce to cast the given value into a integer before
validation.
iex> Z.integer(coerce: true)
iex> |> Z.parse("123")
{:ok, 123}
iex> Z.integer(coerce: true)
iex> |> Z.parse("123.678")
{:ok, 123}If a float is provided, it will be truncated to an integer.
iex> Z.integer(coerce: true)
iex> |> Z.parse(123.678)
{:ok, 123}
@spec length(type, length :: non_neg_integer(), opts :: [{:error, String.t()}]) :: Zodish.Type.List.t() when type: Zodish.Type.List.t()
@spec length(type, length :: non_neg_integer(), opts :: [{:error, String.t()}]) :: Zodish.Type.String.t() when type: Zodish.Type.String.t()
Updates the given type's :length option.
iex> Z.integer()
iex> |> Z.list(length: 1)
iex> |> Z.length(2)
iex> |> Z.parse([1])
{:error, %Zodish.Issue{message: "expected list to have exactly 2 items, got 1 item"}}
iex> Z.string(length: 5)
iex> |> Z.length(1)
iex> |> Z.parse("Hello")
{:error, %Zodish.Issue{message: "expected string to have exactly 1 character, got 5 characters"}}
@spec list(inner_type :: Zodish.Type.t(), opts :: [option]) :: Zodish.Type.List.t() when option: {:length, non_neg_integer()} | {:length, Zodish.Option.t(non_neg_integer())} | {:min, non_neg_integer()} | {:min, Zodish.Option.t(non_neg_integer())} | {:max, non_neg_integer()} | {:max, Zodish.Option.t(non_neg_integer())}
Defines a list type.
iex> Z.list(Z.integer())
iex> |> Z.parse([1, 2, 3])
{:ok, [1, 2, 3]}
iex> Z.list(Z.integer())
iex> |> Z.parse([1, 2, "3"])
{:error, %Zodish.Issue{
message: "one or more items of the list did not match the expected type",
parse_score: 3,
issues: [%Zodish.Issue{path: ["2"], message: "expected an integer, got string"}]
}}Options
You can use :length, :min and :max to
constrain the length of the list.
iex> Z.list(Z.integer(), length: 3)
iex> |> Z.parse([1, 2, 3, 4])
{:error, %Zodish.Issue{message: "expected list to have exactly 3 items, got 4 items"}}
iex> Z.list(Z.integer(), min: 1)
iex> |> Z.parse([])
{:error, %Zodish.Issue{message: "expected list to have at least 1 item, got 0 items"}}
iex> Z.list(Z.integer(), max: 3)
iex> |> Z.parse([1, 2, 3, 4])
{:error, %Zodish.Issue{message: "expected list to have at most 3 items, got 4 items"}}
@spec literal(value :: any()) :: Zodish.Type.Literal.t()
Defines a type that only accepts a specific value.
iex> Z.literal("foo")
iex> |> Z.parse("foo")
{:ok, "foo"}
iex> Z.literal(42)
iex> |> Z.parse(51)
{:error, %Zodish.Issue{message: "expected to be exactly 42, got 51"}}
@spec map(mode, shape) :: Zodish.Type.Map.t() when mode: :strip | :strict, shape: %{required(atom()) => Zodish.Type.t()}
@spec map([option, ...], shape) :: Zodish.Type.Map.t() when option: {:coerce, boolean()} | {:mode, :strip | :strict}, shape: %{required(atom()) => Zodish.Type.t()}
Defines a map type.
iex> Z.map(%{name: Z.string(), age: Z.integer(gte: 18)})
iex> |> Z.parse(%{name: "John Doe", age: 27})
{:ok, %{name: "John Doe", age: 27}}The keys of the parsed map will always be atoms.
iex> Z.map(%{name: Z.string(), age: Z.integer(gte: 18)})
iex> |> Z.parse(%{"name" => "John Doe", "age" => 27})
{:ok, %{name: "John Doe", age: 27}}Options
You can specify one of two behaviors for how to handle unknown fields in the input value:
:strip(default) - Unknown fields will be ignored and not included in the parsed result;:strict- Unknown fields will cause a validation error.iex> Z.map(:strip, %{name: Z.string(), age: Z.integer(gte: 18)}) iex> |> Z.parse(%{name: "John Doe", email: "johndoe@gmail.com", age: 27}) {:ok, %{name: "John Doe", age: 27}}
iex> Z.map(:strict, %{name: Z.string(), age: Z.integer(gte: 18)}) iex> |> Z.parse(%{name: "John Doe", email: "johndoe@gmail.com", age: 27}) {:error, %Zodish.Issue{
message: "one or more fields failed validation", parse_score: 3, issues: [%Zodish.Issue{path: ["email"], message: "unknown field"}]}}
You can also use :coerce to cast values from struct or keyword lists to maps before validation:
iex> Z.coerce(Z.map(:strip, %{name: Z.string(), age: Z.integer(gte: 18)}))
iex> |> Z.parse(name: "John Doe", email: "johndoe@gmail.com", age: 27)
{:ok, %{name: "John Doe", age: 27}}If you need to validate a map where you don't know what keys will be
present, then use Z.record/1 instead.
@spec max(type, length :: non_neg_integer(), opts :: [{:error, String.t()}]) :: Zodish.Type.List.t() when type: Zodish.Type.List.t()
@spec max(type, length :: non_neg_integer(), opts :: [{:error, String.t()}]) :: Zodish.Type.String.t() when type: Zodish.Type.String.t()
Updates the given type's :max option.
iex> Z.integer()
iex> |> Z.list(max: 3)
iex> |> Z.max(2)
iex> |> Z.parse([1, 2, 3])
{:error, %Zodish.Issue{message: "expected list to have at most 2 items, got 3 items"}}
iex> Z.string(max: 3)
iex> |> Z.max(1)
iex> |> Z.parse("Foo")
{:error, %Zodish.Issue{message: "expected string to have at most 1 character, got 3 characters"}}
@spec merge(a :: Zodish.Type.Map.t(), b :: Zodish.Type.Map.t()) :: Zodish.Type.Map.t()
@spec merge(a :: Zodish.Type.Struct.t(), b :: Zodish.Type.Struct.t()) :: Zodish.Type.Struct.t()
Merges two Map types into one, where :mode is inherited from the
most strict mode between the two given types.
iex> a = Z.map(:strip, %{name: Z.string()})
iex> b = Z.map(:strict, %{age: Z.integer()})
iex>
iex> Z.merge(a, b)
iex> |> Z.parse(%{name: "John Doe", age: 27, email: "johndoe@gmail.com"})
{:error, %Zodish.Issue{
message: "one or more fields failed validation",
parse_score: 3,
issues: [%Zodish.Issue{path: ["email"], message: "unknown field"}]
}}
iex> a = Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string()
iex> })
iex>
iex> b = Z.struct(Address, %{
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> })
iex>
iex> Z.merge(a, b)
iex> |> Z.parse(%{
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
line_1: "123 Main St",
line_2: "Apt 4B",
city: "Springfield",
state: "IL",
zip: "62701"
}}
@spec min(type, length :: non_neg_integer(), opts :: [{:error, String.t()}]) :: Zodish.Type.List.t() when type: Zodish.Type.List.t()
@spec min(type, length :: non_neg_integer(), opts :: [{:error, String.t()}]) :: Zodish.Type.String.t() when type: Zodish.Type.String.t()
Updates the given type's :min option.
iex> Z.integer()
iex> |> Z.list(min: 1)
iex> |> Z.min(2)
iex> |> Z.parse([1])
{:error, %Zodish.Issue{message: "expected list to have at least 2 items, got 1 item"}}
iex> Z.string(min: 1)
iex> |> Z.min(6)
iex> |> Z.parse("Foo")
{:error, %Zodish.Issue{message: "expected string to have at least 6 characters, got 3 characters"}}
@spec number(opts :: [option]) :: Zodish.Type.Integer.t() when option: {:coerce, boolean()} | {:gt, number()} | {:gt, Zodish.Option.t(number())} | {:gte, number()} | {:gte, Zodish.Option.t(number())} | {:lt, number()} | {:lt, Zodish.Option.t(number())} | {:lte, number()} | {:lte, Zodish.Option.t(number())}
Defines a number type.
iex> Z.number()
iex> |> Z.parse(3)
{:ok, 3}
iex> Z.number()
iex> |> Z.parse(3.14)
{:ok, 3.14}Options
You can use :gt, :gte, :lt and :lte to constrain the allowed
values.
iex> Z.number(gt: 0)
iex> |> Z.parse(0)
{:error, %Zodish.Issue{message: "expected a number greater than 0, got 0"}}
iex> Z.number(gte: 0)
iex> |> Z.parse(-1)
{:error, %Zodish.Issue{message: "expected a number greater than or equal to 0, got -1"}}
iex> Z.number(lt: 1)
iex> |> Z.parse(2)
{:error, %Zodish.Issue{message: "expected a number less than 1, got 2"}}
iex> Z.number(lte: 1)
iex> |> Z.parse(2)
{:error, %Zodish.Issue{message: "expected a number less than or equal to 1, got 2"}}You can use :coerce to cast the given value into a number before
validation.
iex> Z.number(coerce: true)
iex> |> Z.parse("123")
{:ok, 123}
iex> Z.number(coerce: true)
iex> |> Z.parse("123.678")
{:ok, 123.678}
@spec numeric(opts :: [option]) :: Zodish.Type.String.t() when option: {:length, non_neg_integer()} | {:length, Zodish.Option.t(non_neg_integer())} | {:min, non_neg_integer()} | {:min, Zodish.Option.t(non_neg_integer())} | {:max, non_neg_integer()} | {:max, Zodish.Option.t(non_neg_integer())}
Defines a numeric string type.
iex> Z.numeric()
iex> |> Z.parse("123456")
{:ok, "123456"}
iex> Z.numeric()
iex> |> Z.parse("a1b2c3")
{:error, %Zodish.Issue{message: "must contain 0-9 digits only"}}You can constrain the number of digits by using the :length,
:min and :max options:
iex> Z.numeric(length: 3)
iex> |> Z.parse("1234")
{:error, %Zodish.Issue{message: "expected numeric string to have exactly 3 digits, got 4 digits"}}
iex> Z.numeric(min: 2)
iex> |> Z.parse("1")
{:error, %Zodish.Issue{message: "expected numeric string to have at least 2 digits, got 1 digit"}}
iex> Z.numeric(max: 3)
iex> |> Z.parse("1234")
{:error, %Zodish.Issue{message: "expected numeric string to have at most 3 digits, got 4 digits"}}
@spec omit(type, keys :: [atom()]) :: Zodish.Type.Map.t() when type: Zodish.Type.Map.t()
@spec omit(type, keys :: [atom()]) :: Zodish.Type.Struct.t() when type: Zodish.Type.Struct.t()
Removes the specified keys from the type's shape.
iex> Z.map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.omit([:age])
iex> |> Z.parse(%{name: "John Doe"})
{:ok, %{name: "John Doe"}}
iex> Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> })
iex> |> Z.omit([:line_1, :line_2])
iex> |> Z.parse(%{
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
city: "Springfield",
state: "IL",
zip: "62701"
}}
@spec optional(inner_type :: Zodish.Type.t(), opts :: [option]) :: Zodish.Type.Optional.t() when option: {:default, (-> any()) | any() | nil}
Makes a given inner type optional, where you can also define a
default value to be used when the actual value resolves to nil.
iex> Z.integer()
iex> |> Z.parse(nil)
{:error, %Zodish.Issue{message: "is required"}}
iex> Z.optional(Z.integer())
iex> |> Z.parse(nil)
{:ok, nil}Options
You can use :default to define a default value to be used when
the actual value resolves to nil.
iex> Z.optional(Z.integer(), default: 42)
iex> |> Z.parse(nil)
{:ok, 42}
iex> Z.optional(Z.integer(), default: fn -> 42 end)
iex> |> Z.parse(nil)
{:ok, 42}The default value must satisfy the inner type of the Zodish.Type.Optional.
If you provide a default value other than a function that doesn't
satisfy the inner type, it will raise an ArgumentError.
iex> Z.optional(Z.integer(), default: "not a number")
** (ArgumentError) The default value must satisfy the inner type of Zodish.Type.OptionalIf you provide a function as the default value though and it returns
a value that doesn't satisfy the inner type, it will return a
Zodish.Issue since this check cannot be done at compile time.
iex> Z.optional(Z.integer(), default: fn -> "abc" end)
iex> |> Z.parse(nil)
{:error, %Zodish.Issue{message: "expected an integer, got string"}}If you're defining your schema at compile time into a compiled variable, then you won't be able to use an anonymous function as the default value. So instead you can pass an mfa tuple of the function that should be called to get the default value:
iex> Z.optional(Z.integer(), default: {Echo, :say, [42]})
iex> |> Z.parse(nil)
{:ok, 42}If you call optional/1 on an already optional type, it will just
return the same type back:
iex> Z.string()
iex> |> Z.optional()
iex> |> Z.optional()
%Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: nil}
iex> Z.string()
iex> |> Z.optional(default: "foo")
iex> |> Z.optional()
%Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: "foo"}If you call optional/2 though (with a default value), it will
override the existing default value:
iex> Z.string()
iex> |> Z.optional()
iex> |> Z.optional(default: "foo")
%Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: "foo"}
@spec optional?(type :: Zodish.Type.t()) :: boolean()
Checks whether the given type is an optional type.
iex> Z.string()
iex> |> Z.optional()
iex> |> Z.optional?()
true
iex> Z.string()
iex> |> Z.optional?()
falseIt also works if the type is wrapped in refinements and/or transformations:
iex> Z.string()
iex> |> Z.optional()
iex> |> Z.refine(fn _ -> true end)
iex> |> Z.refine(fn _ -> true end)
iex> |> Z.transform(fn value -> value end)
iex> |> Z.optional?()
true
@spec parse(type :: Zodish.Type.t(), value :: any()) :: {:ok, any()} | {:error, Zodish.Issue.t()}
Parses a value based on the given type.
iex> Z.string()
iex> |> Z.parse("Hello, World!")
{:ok, "Hello, World!"}
@spec parse!(type, value) :: term() when type: Zodish.Type.t(), value: term()
Same as parse/1 but raises an error if failed to parse the given
params.
iex> Z.string()
iex> |> Z.parse!("Hello, World!")
"Hello, World!"
iex> Z.string()
iex> |> Z.parse!(123)
** (Zodish.Issue) expected a string, got integer
@spec partial(type) :: type when type: Zodish.Type.Map.t() | Zodish.Type.Struct.t() | shaped()
Makes all fields from the given type's shape optional (default nil).
iex> Z.map(%{line_1: Z.string(), line_2: Z.string()})
iex> |> Z.partial()
iex> |> Map.get(:shape)
%{
line_1: %Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: nil},
line_2: %Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: nil}
}
iex> Z.struct(Address, %{line_1: Z.string(), line_2: Z.string()})
iex> |> Z.partial()
iex> |> Map.get(:shape)
%{
line_1: %Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: nil},
line_2: %Zodish.Type.Optional{inner_type: %Zodish.Type.String{}, default: nil}
}
@spec pick(type, keys :: [atom()]) :: Zodish.Type.Map.t() when type: Zodish.Type.Map.t()
@spec pick(type, keys :: [atom()]) :: Zodish.Type.Struct.t() when type: Zodish.Type.Struct.t()
Keeps only the specified keys from the type's shape.
iex> Z.map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.pick([:name])
iex> |> Z.parse(%{name: "John Doe"})
{:ok, %{name: "John Doe"}}
iex> Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> })
iex> |> Z.pick([:city, :state, :zip])
iex> |> Z.parse(%{
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
city: "Springfield",
state: "IL",
zip: "62701"
}}
@spec record(opts :: [option]) :: Zodish.Type.Record.t() when option: {:keys, Zodish.Type.t()} | {:values, Zodish.Type.t()}
Defines a record type.
iex> Z.record()
iex> |> Z.parse(%{"foo" => "bar"})
{:ok, %{"foo" => "bar"}}Options
You can use the option :keys to default a schema for the keys in
the record.
iex> Z.record(keys: Z.string(min: 1))
iex> |> Z.parse(%{foo: "bar"})
{:error, %Zodish.Issue{
path: [],
message: "one or more fields failed validation",
parse_score: 1,
issues: [%Zodish.Issue{path: ["foo"], message: "expected a string, got atom"}]
}}Although you can specify a schema for the keys, it must be a string type.
iex> Z.record(keys: Z.integer())
** (ArgumentError) Record keys must be stringYou can use the option :values to set a schema that will be used
to parse the values in the record.
iex> Z.record(values: Z.string(min: 1))
iex> |> Z.parse(%{"foo" => ""})
{:error, %Zodish.Issue{
path: [],
message: "one or more fields failed validation",
parse_score: 1,
issues: [%Zodish.Issue{path: ["foo"], message: "expected string to have at least 1 character, got 0 characters"}],
}}Alternatively you can pass a type as single argument to Z.record/1
where it will be used as the :values types:
iex> Z.record(Z.string(min: 1))
iex> |> Z.parse(%{"foo" => ""})
{:error, %Zodish.Issue{
path: [],
message: "one or more fields failed validation",
parse_score: 1,
issues: [%Zodish.Issue{path: ["foo"], message: "expected string to have at least 1 character, got 0 characters"}],
}}
@spec refine(inner_type, fun, opts :: [option]) :: Zodish.Type.Refine.t() when inner_type: Zodish.Type.t(), fun: (any() -> boolean()) | mfa(), option: {:error, String.t()}
Refines a value with a custom validation.
iex> is_even = fn x -> rem(x, 2) == 0 end
iex>
iex> Z.integer()
iex> |> Z.refine(is_even)
iex> |> Z.parse(3)
{:error, %Zodish.Issue{message: "is invalid", parse_score: 1}}Options
You can use the options :error to set a custom error message that
will be used when the validation fails.
iex> is_even = fn x -> rem(x, 2) == 0 end
iex>
iex> Z.integer()
iex> |> Z.refine(is_even, error: "must be even")
iex> |> Z.parse(3)
{:error, %Zodish.Issue{message: "must be even", parse_score: 1}}
@spec strict(Zodish.Type.Map.t()) :: Zodish.Type.Map.t()
@spec strict(Zodish.Type.Struct.t()) :: Zodish.Type.Struct.t()
Switches the mode of the given schema to :strict, where additional fields are not allowed.
iex> Z.strict(Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> }))
iex> |> Z.parse(%{
iex> name: "John Doe",
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:error, %Zodish.Issue{
message: "one or more fields failed validation",
parse_score: 6,
issues: [%Zodish.Issue{path: ["name"], message: "unknown field"}]
}}Worth noting that :strict is the default mode for Z.struct/2.
@spec string(opts :: [option]) :: Zodish.Type.String.t() when option: {:coerce, boolean()} | {:trim, boolean()} | {:downcase, boolean()} | {:upcase, boolean()} | {:length, non_neg_integer()} | {:length, Zodish.Option.t(non_neg_integer())} | {:min, non_neg_integer()} | {:min, Zodish.Option.t(non_neg_integer())} | {:max, non_neg_integer()} | {:max, Zodish.Option.t(non_neg_integer())} | {:starts_with, String.t()} | {:starts_with, Zodish.Option.t(String.t())} | {:ends_with, String.t()} | {:ends_with, Zodish.Option.t(String.t())} | {:regex, Regex.t()} | {:regex, Zodish.Option.t(Regex.t())}
Defines a string type.
iex> Z.string()
iex> |> Z.parse("Hello, World!")
{:ok, "Hello, World!"}Options
You can use :length, :min and :max to
constrain the length of the string.
iex> Z.string(length: 3)
iex> |> Z.parse("foobar")
{:error, %Zodish.Issue{message: "expected string to have exactly 3 characters, got 6 characters"}}
iex> Z.string(min: 1)
iex> |> Z.parse("")
{:error, %Zodish.Issue{message: "expected string to have at least 1 character, got 0 characters"}}
iex> Z.string(max: 3)
iex> |> Z.parse("foobar")
{:error, %Zodish.Issue{message: "expected string to have at most 3 characters, got 6 characters"}}You can also use :trim to trim leading and trailing whitespaces
from the string before validation.
iex> Z.string(trim: true, min: 1)
iex> |> Z.parse(" ")
{:error, %Zodish.Issue{message: "expected string to have at least 1 character, got 0 characters"}}You can use :starts_with and :ends_with to check if the string
starts with a given prefix or ends with a given suffix.
iex> Z.string(starts_with: "sk_")
iex> |> Z.parse("pk_123")
{:error, %Zodish.Issue{message: "expected string to start with \"sk_\", got \"pk_123\""}}
iex> Z.string(ends_with: "bar")
iex> |> Z.parse("fizzbuzz")
{:error, %Zodish.Issue{message: "expected string to end with \"bar\", got \"fizzbuzz\""}}You can use :regex to validate the string against a regular
expression.
iex> Z.string(regex: ~r/^\d+$/)
iex> |> Z.parse("123abc")
{:error, %Zodish.Issue{message: "expected string to match /^\\d+$/, got \"123abc\""}}You can use :coerce to cast the given value into a string before
validation.
iex> Z.string(coerce: true)
iex> |> Z.parse(123)
{:ok, "123"}
@spec strip(Zodish.Type.Map.t()) :: Zodish.Type.Map.t()
@spec strip(Zodish.Type.Struct.t()) :: Zodish.Type.Struct.t()
Switches the mode of the given schema to :strip, where additional fields are ignored.
iex> Z.strip(Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> }))
iex> |> Z.parse(%{
iex> name: "John Doe",
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
line_1: "123 Main St",
line_2: "Apt 4B",
city: "Springfield",
state: "IL",
zip: "62701"
}}Worth noting that :strip is the default mode for Z.map/1.
@spec struct(mod, shape) :: Zodish.Type.Struct.t() when mod: module(), shape: %{required(atom()) => Zodish.Type.t()}
@spec struct([option, ...], shape) :: Zodish.Type.Struct.t() when option: {:module, module()} | {:mode, :strict | :strip}, shape: %{required(atom()) => Zodish.Type.t()}
Defines a struct type.
iex> Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> })
iex> |> Z.parse(%{
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
line_1: "123 Main St",
line_2: "Apt 4B",
city: "Springfield",
state: "IL",
zip: "62701"
}}If your Zodish type includes a key that doesn't exist in the struct,
then an ArgumentError will be raised.
iex> Z.struct(Address, %{name: Z.string()})
** (ArgumentError) The shape key :name doesn't exist in struct ZodishTest.AddressA Zodish struct type works like a Map type in :strict mode, meaning if a field that isn't present in the struct is provided in the input, then it will fail validation.
iex> Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> })
iex> |> Z.parse(%{
iex> name: "John Doe",
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:error, %Zodish.Issue{
message: "one or more fields failed validation",
parse_score: 6,
issues: [%Zodish.Issue{path: ["name"], message: "unknown field"}]
}}In Zodish.struct/2 the schema is :strict by default, meaning that
additional fields are not allowed. If you want it to behave as
:strip, like it's available to Zodish.map/2, you can use on of the
following options:
Option 1:
iex> Z.struct([module: Address, mode: :strip], %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> })
iex> |> Z.parse(%{
iex> name: "John Doe",
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
line_1: "123 Main St",
line_2: "Apt 4B",
city: "Springfield",
state: "IL",
zip: "62701"
}}Option 2:
iex> Z.strip(Z.struct(Address, %{
iex> line_1: Z.string(),
iex> line_2: Z.string(),
iex> city: Z.string(),
iex> state: Z.string(),
iex> zip: Z.string(),
iex> }))
iex> |> Z.parse(%{
iex> name: "John Doe",
iex> line_1: "123 Main St",
iex> line_2: "Apt 4B",
iex> city: "Springfield",
iex> state: "IL",
iex> zip: "62701"
iex> })
{:ok, %Address{
line_1: "123 Main St",
line_2: "Apt 4B",
city: "Springfield",
state: "IL",
zip: "62701"
}}
@spec to_spec(type) :: Macro.t() when type: Zodish.Type.t()
Infers the type spec of a given Zodish type to be used with @type.
Z.spec(type)Examples
Unquote the returned value of Z.spec/1 into your @type definition,
Zodish will pick the most readable way to represent your types:
@schema Z.map(%{
name: Z.string(),
email: Z.email()
})
@type t() :: unquote(Z.spec(@schema))Type any
iex> Z.any() |> Z.to_spec()
quote(do: any())Type atom
iex> Z.atom() |> Z.to_spec()
quote(do: atom())Type boolean
iex> Z.boolean() |> Z.to_spec()
quote(do: boolean())Type date
iex> Z.date() |> Z.to_spec()
quote(do: Date.t())Type datetime
iex> Z.datetime() |> Z.to_spec()
quote(do: DateTime.t())Type decimal
iex> Z.decimal() |> Z.to_spec()
quote(do: Decimal.t())Type email
iex> Z.email() |> Z.to_spec()
quote(do: String.t())Type enum
iex> Z.enum([:foo, :bar, :baz]) |> Z.to_spec()
quote(do: :foo | :bar | :baz)Type float
iex> Z.float() |> Z.to_spec()
quote(do: float())Type integer
iex> Z.integer() |> Z.to_spec()
quote(do: integer())
iex> Z.integer(gt: 0) |> Z.to_spec()
quote(do: pos_integer())
iex> Z.integer(gte: 0) |> Z.to_spec()
quote(do: non_neg_integer())
iex> Z.integer(gt: 0, lt: 10) |> Z.to_spec()
{:.., [], [1, 9]}
iex> Z.integer(gte: 0, lte: 10) |> Z.to_spec()
{:.., [], [0, 10]}Type list
iex> Z.list(Z.integer()) |> Z.to_spec()
quote(do: [integer()])
iex> Z.list(Z.integer()) |> Z.min(1) |> Z.to_spec()
quote(do: [integer(), ...])Type literal
iex> Z.literal(:foo) |> Z.to_spec()
:foo
iex> Z.literal(true) |> Z.to_spec()
true
iex> Z.literal(false) |> Z.to_spec()
false
iex> Z.literal(~D[2025-10-31]) |> Z.to_spec()
quote(do: Date.t())
iex> Z.literal(~U[2025-10-31T00:00:00.000Z]) |> Z.to_spec()
quote(do: DateTime.t())
iex> Z.literal(Decimal.new(0)) |> Z.to_spec()
quote(do: Decimal.t())
iex> Z.literal(3.14) |> Z.to_spec()
quote(do: float())
iex> Z.literal(-1) |> Z.to_spec()
quote(do: integer())
iex> Z.literal(0) |> Z.to_spec()
quote(do: non_neg_integer())
iex> Z.literal(123) |> Z.to_spec()
quote(do: pos_integer())
iex> Z.literal([foo: 1, bar: 2]) |> Z.to_spec()
quote(do: keyword())
iex> Z.literal([1, 2, 3]) |> Z.to_spec()
quote(do: list())
iex> Z.literal(%{foo: :bar}) |> Z.to_spec()
quote(do: map())
iex> Z.literal("foo") |> Z.to_spec()
quote(do: String.t())
iex> Z.literal(%Address{}) |> Z.to_spec()
{:%, [], [{:__aliases__, [alias: false], [ZodishTest.Address]}, {:%{}, [], []}]}
iex> Z.literal(%{__struct__: ZodishTest.Address}) |> Z.to_spec()
{:%, [], [{:__aliases__, [alias: false], [ZodishTest.Address]}, {:%{}, [], []}]}
iex> Z.literal({:ok, :foo, :bar}) |> Z.to_spec()
quote(do: {:ok, :foo, :bar})
iex> Z.literal("http://localhost:3000/") |> Z.to_spec()
quote(do: String.t())
iex> Z.literal("4c5995ef-1bd6-43a7-86a6-e721a3e2d99a") |> Z.to_spec()
quote(do: String.t())Type map
iex> Z.map(%{
iex> foo: Z.string(),
iex> bar: Z.integer()
iex> })
iex> |> Z.to_spec()
quote(do: %{foo: String.t(), bar: integer()})
iex> Z.map(%{
iex> foo: Z.string(),
iex> bar: Z.optional(Z.integer())
iex> })
iex> |> Z.to_spec()
quote(do: %{foo: String.t(), bar: integer() | nil})
iex> Z.map(%{
iex> foo: Z.string(),
iex> bar: Z.optional(Z.integer(), default: 10)
iex> })
iex> |> Z.to_spec()
quote(do: %{foo: String.t(), bar: integer()})Type number
iex> Z.number() |> Z.to_spec()
quote(do: number())Type numeric
iex> Z.numeric() |> Z.to_spec()
quote(do: String.t())Type optional
iex> Z.string() |> Z.optional() |> Z.to_spec()
quote(do: String.t() | nil)
iex> Z.string() |> Z.optional(default: "foo") |> Z.to_spec()
quote(do: String.t())Type record
iex> Z.record(keys: Z.string(), values: Z.integer()) |> Z.to_spec()
{:%{}, [], [{{:optional, [], [quote(do: String.t())]}, quote(do: integer())}]}Type refine
iex> Z.integer()
iex> |> Z.refine(&(&1 > 0), error: "must be positive")
iex> |> Z.to_spec()
quote(do: integer())
iex> Z.string()
iex> |> Z.refine(&String.starts_with?(&1, "A"), error: "must start with A")
iex> |> Z.to_spec()
quote(do: String.t())Type string
iex> Z.string() |> Z.to_spec()
{{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}Type struct
iex> Z.struct(Address, %{
iex> line_1: Z.string(min: 1, max: 100),
iex> line_2: Z.optional(Z.string(min: 1, max: 100)),
iex> })
iex> |> Z.to_spec()
{:%, [], [{:__aliases__, [alias: false], [ZodishTest.Address]}, {:%{}, [], [line_1: quote(do: String.t()), line_2: quote(do: String.t() | nil)]}]}Type tuple
iex> Z.tuple([Z.literal(:error), Z.string(min: 1)])
iex> |> Z.to_spec()
{:{}, [], [:error, {{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}]}Type union
iex> Z.union([Z.literal(:ok), Z.literal(:error)])
iex> |> Z.to_spec()
{:|, [], [:ok, :error]}Type uri
iex> Z.uri() |> Z.to_spec()
{{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}Type uuid
iex> Z.uuid() |> Z.to_spec()
{:<<>>, [], [{:"::", [], [{:_, [], Elixir}, 288]}]}
@spec transform(inner_type, fun) :: Zodish.Type.Transform.t() when inner_type: Zodish.Type.t(), fun: (any() -> any())
Transforms the parsed value using a given function.
iex> Z.integer()
iex> |> Z.transform(fn x -> x * 2 end)
iex> |> Z.parse(3)
{:ok, 6}Alternatively, a {mod, fun, args} tuple can be provided indicating
what function should be invoked. The function is invoked with the
Zodish value as its first argument followed by the args you
provided.
iex> Z.integer()
iex> |> Z.transform({Integer, :to_string, []})
iex> |> Z.parse(10)
{:ok, "10"}
@spec tuple(elements :: [Zodish.Type.t(), ...]) :: Zodish.Type.Tuple.t()
Defines a tuple type.
iex> Z.tuple([Z.atom(), Z.integer()])
iex> |> Z.parse({:ok, 123})
{:ok, {:ok, 123}}
iex> Z.tuple([Z.atom(), Z.integer(), Z.string()])
iex> |> Z.parse({:ok, "abc"})
{:error, %Zodish.Issue{message: "expected a tuple of length 3, got length 2"}}
iex> Z.tuple([Z.atom(), Z.integer()])
iex> |> Z.parse({:ok, "abc"})
{:error, %Zodish.Issue{
message: "one or more elements of the tuple did not match the expected type",
parse_score: 1,
issues: [%Zodish.Issue{path: ["1"], message: "expected an integer, got string"}],
}}
@spec union(inner_types) :: Zodish.Type.Union.t() when inner_types: [Zodish.Type.t(), ...]
Defines a union type of 2 or more schemas.
iex> Z.union([
iex> Z.string(),
iex> Z.integer()
iex> ])
iex> |> Z.parse("Hello, World!")
{:ok, "Hello, World!"}
iex> Z.union([
iex> Z.string(),
iex> Z.integer()
iex> ])
iex> |> Z.parse(23.45)
{:error, %Zodish.Issue{message: "expected an integer, got float"}}The resulting error will be from the schema which made the most progress parsing the value.
iex> a = Z.map(%{foo: Z.string(), bar: Z.integer(), baz: Z.boolean()})
iex> b = Z.map(%{foo: Z.string(), qux: Z.float()})
iex>
iex> Z.union([a, b])
iex> |> Z.parse(%{foo: "Hello", bar: 123})
{:error, %Zodish.Issue{message: "one or more fields failed validation", parse_score: 3, issues: [
%Zodish.Issue{path: ["baz"], message: "is required"}
]}}Warning
The logic for selecting the best schema validation issues is still a work in progress and may be changed in the future.
@spec uri([option]) :: Zodish.Type.URI.t() when option: {:schemes, [String.t()]} | {:trailing_slash, :keep | :trim | :enforce}
Defines a string URI type.
iex> Z.uri()
iex> |> Z.parse("https://foo.bar/")
{:ok, "https://foo.bar/"}Options
You can constrain which schemes are allowed by passing the
:schemes option:
iex> Z.uri(schemes: ["https"])
iex> |> Z.parse("http://localhost/")
{:error, %Zodish.Issue{message: "scheme not allowed"}}You can trim the trailing slash in the uri by passing the
:trailing_slash option:
iex> Z.uri(trailing_slash: :trim)
iex> |> Z.parse("https://foo.bar/api/")
{:ok, "https://foo.bar/api"}Likewise, you can enfore the trailing slash:
iex> Z.uri(trailing_slash: :enforce)
iex> |> Z.parse("https://foo.bar/api")
{:ok, "https://foo.bar/api/"}
Defines a UUID type (decorated String type).
iex> Z.uuid()
iex> |> Z.parse("550e8400-e29b-41d4-a716-446655440000")
{:ok, "550e8400-e29b-41d4-a716-446655440000"}You can optionally specify which version of UUID to validate for.
iex> Z.uuid(:any)
iex> |> Z.parse("550e8400-e29b-41d4-a716-446655440000")
{:ok, "550e8400-e29b-41d4-a716-446655440000"}
iex> Z.uuid(:v4)
iex> |> Z.parse("67ef5479-e5c2-411f-9cfc-82ff3c17a76e")
{:ok, "67ef5479-e5c2-411f-9cfc-82ff3c17a76e"}
iex> Z.uuid(:v7)
iex> |> Z.parse("019798df-04e0-7279-8bca-26f70bb361d2")
{:ok, "019798df-04e0-7279-8bca-26f70bb361d2"}There are 9 supported options: :any (default), :v1, :v2, :v3,
:v4, :v5, :v6, :v7 and :v8.