Zodish (zodish v0.2.4)

View Source

Zodish 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.

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

shaped()

@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

any()

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

atom(opts \\ [])

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

boolean(opts \\ [])

@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:

valuecoerced to
"true"true
"1"true
1true
"yes"true
"y"true
"on"true
"enabled"true
"false"false
"0"false
0false
"no"false
"n"false
"off"false
"disabled"false

coerce(type, value \\ true)

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

date(opts \\ [])

@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]}

datetime(opts \\ [])

@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))

decimal(opts \\ [])

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

email(opts \\ [])

@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 validate input[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"}}

enum(opts)

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

float(opts \\ [])

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

integer(opts \\ [])

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

length(type, length, opts \\ [])

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

list(inner_type, opts \\ [])

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

literal(value)

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

map(mode_or_opts \\ :strip, shape)

@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.

max(type, length, opts \\ [])

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

merge(a, b)

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

min(type, length, opts \\ [])

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

number(opts \\ [])

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

numeric(opts \\ [])

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

omit(type, keys)

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

optional(inner_type, opts \\ [])

@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.Optional

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

optional?(arg1)

@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?()
false

It 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

parse(type, value)

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

parse!(type, value)

@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

partial(type)

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

pick(type, keys)

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

record(opts \\ [])

@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 string

You 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"}],
}}

refine(inner_type, fun, opts \\ [])

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

strict(type)

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.

string(opts \\ [])

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

strip(type)

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.

struct(mod_or_opts, shape)

@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.Address

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

to_spec(type)

@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]}]}

transform(inner_type, fun)

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

tuple(elements)

@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"}],
}}

union(schemas)

@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.

uri(opts \\ [])

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

uuid(version \\ :any)

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.