Zot (zot v0.12.0)
View SourceSchema parser and validator for Elixir.
Summary
Functions
Controls whether loopback addresses are allowed in URLs.
Constraint phone numbers to a limited set of country codes.
Restricts which ports are allowed in a URL.
Enforces that the URL has one of the given allowed schemes.
Creates a type that accepts any value.
Creates an atom type.
Creates a boolean type.
Wraps the parsed value in a branded tuple.
Enables automatic canonicalization for CIDR notation.
Creates a CIDR notation type for IPv4 or IPv6 network addresses.
Validates that the IP address falls within the given CIDR range(s).
Enforces that the string contains the given substring.
Creates a date type.
Creates a date-time type.
Alias to date_time/1.
Creates a decimal type.
Sets the field as not-required and provides a default value.
Attaches a description to the type, for use in JSON Schema.
Creates a discriminated union of two or more map types.
Alias to date_time/1.
Creates an email type.
Enforces that the string ends with the given substring.
Creates an enum type.
Attaches an example value to the type, for use in JSON Schema.
Creates a float type.
Restricts which ports are forbidden in a URL.
Alias to integer/1.
Creates a integer type.
Creates an IP address type.
Converts the given type into a JSON Schema.
Defines the behavior for leading plus signs in phone numbers.
Enforces that the string or list has the given length.
Creates a list type.
Creates a literal type.
Creates a map type where unknown fields are stripped out.
Enforces a maximum value for the given type.
Enforces a maximum prefix length for CIDR notation.
Merges two map types into a new map type.
Enforces a minimum value for the given type.
Enforces a minimum prefix length for CIDR notation.
Creates a non-empty string type that trims whitespace.
Creates a number type (union of integer and float types).
Creates a numeric string type.
Creates a new map type excluding the specified keys from the original shape.
Sets the field as not required (nullable).
Sets the output format for the given type.
Parses the given input according to the given type.
Makes all fields optional. Optionally drops all nil fields from the resulting map, after successfully parsed and validate.
Alias for partial/2 with option compact: true.
Creates a phone number type.
Creates a new map type with only the specified keys from the original shape.
Rounds the float to the given number of decimal places.
Renders a list of issues into a pretty-printed string for display.
Defines the behavior regarding query strings in URLs.
Sets both min and max from an Elixir Range.
Creates a record type where keys are non-empty strings.
Adds a custom refinement to the given type's effects pipeline, which is executed after the type is successfully parsed and validated.
Enforces that the string matches the given regex.
Requires that the URL has a non-root path.
Creates a set type (a list of unique items).
Enforces that the string starts with the given substring.
Creates a map type where unknown fields cause an issue.
Creates a string type.
Creates a struct type.
Summarizes a list of issues into a map of paths (dot-notated) to messages.
Alias to date_time/1.
Defines the behavior regarding trailing slashes in URLs.
Adds a transformation to the given type's effects pipeline, which is executed after the type is successfully parsed and validated.
Trims whitespace from the beginning and end of the string before validation.
Alias to date_time/1.
Creates a tuple type with a fixed number of heterogeneous elements.
Unwraps a branded type, returning its inner type.
Creates a union of two or more types.
Creates a URL string type.
Creates a UUID type.
Enforces the version for the given type.
Pattern matches a Zot type struct.
Functions
Controls whether loopback addresses are allowed in URLs.
See url/1 for more details.
Constraint phone numbers to a limited set of country codes.
See phone/1 for more details.
Restricts which ports are allowed in a URL.
See url/1 for more details.
Enforces that the URL has one of the given allowed schemes.
See url/1 for more details.
Creates a type that accepts any value.
Examples
iex> Z.any()
iex> |> Z.parse("hello")
{:ok, "hello"}
iex> Z.any()
iex> |> Z.parse(42)
{:ok, 42}
iex> Z.any()
iex> |> Z.parse(%{foo: "bar"})
{:ok, %{foo: "bar"}}
iex> Z.any()
iex> |> Z.optional()
iex> |> Z.parse(nil)
{:ok, nil}Useful in maps where a field can accept any value:
iex> Z.map(%{name: Z.string(), metadata: Z.any()})
iex> |> Z.parse(%{name: "Alice", metadata: %{role: "admin", tags: [1, 2, 3]}})
{:ok, %{name: "Alice", metadata: %{role: "admin", tags: [1, 2, 3]}}}Supports transform and refine effects:
iex> Z.any()
iex> |> Z.transform(&inspect/1)
iex> |> Z.parse({:ok, 42})
{:ok, "{:ok, 42}"}It can be converted into json schema:
iex> Z.any()
iex> |> Z.describe("Arbitrary metadata.")
iex> |> Z.json_schema()
%{
"description" => "Arbitrary metadata."
}
Creates an atom type.
Examples
iex> Z.atom()
iex> |> Z.parse(:foo)
{:ok, :foo}
iex> Z.atom()
iex> |> Z.parse("bar")
iex> |> unwrap_issue_message()
"expected type atom, got string"With coerce: true, it converts strings to existing atoms only:
iex> Z.atom()
iex> |> Z.parse("foo", coerce: true)
{:ok, :foo}
iex> Z.atom()
iex> |> Z.parse("this_atom_does_not_exist", coerce: true)
iex> |> unwrap_issue_message()
"atom 'this_atom_does_not_exist' does not exist"With coerce: :unsafe, it converts any string to an atom:
iex> Z.atom()
iex> |> Z.parse("some_new_atom", coerce: :unsafe)
{:ok, :some_new_atom}It can be converted into json schema:
iex> Z.atom()
iex> |> Z.describe("A status atom.")
iex> |> Z.example(:active)
iex> |> Z.json_schema()
%{
"type" => "string",
"description" => "A status atom.",
"examples" => ["active"]
}
Alias to boolean/0.
Creates a boolean type.
Examples
iex> Z.boolean()
iex> |> Z.parse(true)
{:ok, true}
iex> Z.boolean()
iex> |> Z.parse("yes")
iex> |> unwrap_issue_message()
"expected type boolean, got string"It can be coerced from boolean-like values:
iex> Z.boolean()
iex> |> Z.parse(1, coerce: true)
{:ok, true}
iex> Z.boolean()
iex> |> Z.parse(0, coerce: true)
{:ok, false}
iex> Z.boolean()
iex> |> Z.parse("true", coerce: true)
{:ok, true}
iex> Z.boolean()
iex> |> Z.parse("false", coerce: true)
{:ok, false}
iex> Z.boolean()
iex> |> Z.parse("on", coerce: true)
{:ok, true}
iex> Z.boolean()
iex> |> Z.parse("off", coerce: true)
{:ok, false}
iex> Z.boolean()
iex> |> Z.parse("enabled", coerce: true)
{:ok, true}
iex> Z.boolean()
iex> |> Z.parse("disabled", coerce: true)
{:ok, false}
iex> Z.boolean()
iex> |> Z.parse("yes", coerce: true)
{:ok, true}
iex> Z.boolean()
iex> |> Z.parse("no", coerce: true)
{:ok, false}Coercion fails for non-boolean-like strings:
iex> Z.boolean()
iex> |> Z.parse("maybe", coerce: true)
iex> |> unwrap_issue_message()
"expected a boolean-like string ('true', 'enabled', 'on', 'yes', 'false', 'disabled', 'off' or 'no'), got 'maybe'"It can be converted into json schema:
iex> Z.boolean()
iex> |> Z.describe("A boolean flag.")
iex> |> Z.example(true)
iex> |> Z.json_schema()
%{
"type" => "boolean",
"description" => "A boolean flag.",
"examples" => [true]
}
Wraps the parsed value in a branded tuple.
Examples
iex> Z.string()
iex> |> Z.branded(:name)
iex> |> Z.parse("Rafael")
{:ok, {:name, "Rafael"}}
Enables automatic canonicalization for CIDR notation.
When enabled, non-canonical CIDR notation (where the IP is not the network address) is automatically converted to canonical form.
See cidr/1 for more details.
Creates a CIDR notation type for IPv4 or IPv6 network addresses.
Examples
iex> Z.cidr()
iex> |> Z.parse("192.168.0.0/24")
{:ok, "192.168.0.0/24"}
iex> Z.cidr()
iex> |> Z.parse("2001:db8::/32")
{:ok, "2001:db8::/32"}
iex> Z.cidr()
iex> |> Z.parse("not-a-cidr")
iex> |> unwrap_issue_message()
"is invalid"You can restrict to a specific IP version:
iex> Z.cidr(version: :v4)
iex> |> Z.parse("192.168.0.0/24")
{:ok, "192.168.0.0/24"}
iex> Z.cidr(version: :v4)
iex> |> Z.parse("2001:db8::/32")
iex> |> unwrap_issue_message()
"must be a valid IPv4 CIDR"
iex> Z.cidr(version: :v6)
iex> |> Z.parse("2001:db8::/32")
{:ok, "2001:db8::/32"}
iex> Z.cidr(version: :v6)
iex> |> Z.parse("192.168.0.0/24")
iex> |> unwrap_issue_message()
"must be a valid IPv6 CIDR"You can change the output format:
iex> Z.cidr(output: :tuple)
iex> |> Z.parse("192.168.1.0/24")
{:ok, {{192, 168, 1, 0}, {192, 168, 1, 255}, 24}}
iex> Z.cidr(output: :map)
iex> |> Z.parse("192.168.1.0/24")
{:ok, %{start: {192, 168, 1, 0}, end: {192, 168, 1, 255}, prefix: 24}}Non-canonical CIDR notation (where the IP is not the network address) is rejected by default:
iex> Z.cidr()
iex> |> Z.parse("192.168.1.100/24")
iex> |> unwrap_issue_message()
"must be in canonical form (network address), got '192.168.1.100/24'"You can enable automatic canonicalization:
iex> Z.cidr(canonicalize: true)
iex> |> Z.parse("192.168.1.100/24")
{:ok, "192.168.1.0/24"}You can enforce minimum and maximum prefix lengths:
iex> Z.cidr(min_prefix: 16)
iex> |> Z.parse("10.0.0.0/8")
iex> |> unwrap_issue_message()
"prefix length must be at least 16, got 8"
iex> Z.cidr(max_prefix: 24)
iex> |> Z.parse("10.0.0.0/28")
iex> |> unwrap_issue_message()
"prefix length must be at most 24, got 28"It supports coercion from tuples and maps:
iex> Z.cidr()
iex> |> Z.parse({{192, 168, 0, 0}, 24}, coerce: true)
{:ok, "192.168.0.0/24"}
iex> Z.cidr()
iex> |> Z.parse(%{ip: {192, 168, 0, 0}, prefix: 24}, coerce: true)
{:ok, "192.168.0.0/24"}It can be converted into json schema:
iex> Z.cidr(version: :v4)
iex> |> Z.describe("An IPv4 network.")
iex> |> Z.example("192.168.0.0/24")
iex> |> Z.json_schema()
%{
"type" => "string",
"pattern" => "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/(?:3[0-2]|[12]?[0-9])$",
"description" => "An IPv4 network.",
"examples" => ["192.168.0.0/24"]
}
Validates that the IP address falls within the given CIDR range(s).
See ip/1 for more details.
Enforces that the string contains the given substring.
Creates a date type.
Examples
iex> Z.date()
iex> |> Z.parse(~D[2024-01-15])
{:ok, ~D[2024-01-15]}
iex> Z.date()
iex> |> Z.parse("foo")
iex> |> unwrap_issue_message()
"expected type Date, got string"You can enforce that the date is after a given date:
iex> Z.date(min: ~D[2024-01-01])
iex> |> Z.parse(~D[2023-12-31])
iex> |> unwrap_issue_message()
"must be after 2024-01-01"You can enforce that the date is before a given date:
iex> Z.date(max: ~D[2023-12-31])
iex> |> Z.parse(~D[2024-01-01])
iex> |> unwrap_issue_message()
"must be before 2023-12-31"It supports coercion from ISO8601 strings:
iex> Z.date()
iex> |> Z.parse("2024-01-15", coerce: true)
{:ok, ~D[2024-01-15]}
iex> Z.date()
iex> |> Z.parse("Jan 15, 2024", coerce: true)
iex> |> unwrap_issue_message()
"must be a valid ISO8601 date string"It can be converted into json schema:
iex> Z.date()
iex> |> Z.describe("A birth date.")
iex> |> Z.example(~D[2024-01-15])
iex> |> Z.json_schema()
%{
"type" => "string",
"format" => "date",
"description" => "A birth date.",
"examples" => ["2024-01-15"]
}
Creates a date-time type.
Examples
iex> Z.date_time()
iex> |> Z.parse(~U[2024-01-01T12:34:56Z])
{:ok, ~U[2024-01-01T12:34:56Z]}
iex> Z.date_time()
iex> |> Z.parse("foo")
iex> |> unwrap_issue_message()
"expected type DateTime, got string"You can enforce that the date-time is after a given date-time:
iex> Z.date_time(min: ~U[2024-01-01 00:00:00Z])
iex> |> Z.parse(~U[2023-12-31 23:59:59Z])
iex> |> unwrap_issue_message()
"must be after 2024-01-01T00:00:00Z"You can enforce that the date-time is before a given date-time:
iex> Z.date_time(max: ~U[2023-12-31 23:59:59Z])
iex> |> Z.parse(~U[2024-01-01 00:00:00Z])
iex> |> unwrap_issue_message()
"must be before 2023-12-31T23:59:59Z"It supports coercion from ISO8601 strings:
iex> Z.date_time()
iex> |> Z.parse("2024-01-01T12:34:56Z", coerce: true)
{:ok, ~U[2024-01-01T12:34:56Z]}
iex> Z.date_time()
iex> |> Z.parse("Mon Jan 12 2026 11:16:30 GMT-0300 (Brasilia Standard Time)", coerce: true)
iex> |> unwrap_issue_message()
"must be a valid ISO8601 date-time string"It can be converted into json schema:
iex> Z.date_time()
iex> |> Z.describe("A timestamp.")
iex> |> Z.example(~U[2026-01-10T10:23:45.123Z])
iex> |> Z.json_schema()
%{
"type" => "string",
"format" => "date-time",
"description" => "A timestamp.",
"examples" => ["2026-01-10T10:23:45.123Z"]
}
Alias to date_time/1.
Creates a decimal type.
Examples
iex> Z.decimal()
iex> |> Z.parse(Decimal.new("123.45"))
{:ok, Decimal.new("123.45")}You can enforce a minimum value:
iex> Z.decimal(min: 10)
iex> |> Z.parse(Decimal.new("9.99"))
iex> |> unwrap_issue_message()
"must be at least 10, got 9.99"You can enforce a maximum value:
iex> Z.decimal(max: 9.99)
iex> |> Z.parse(Decimal.new("10.00"))
iex> |> unwrap_issue_message()
"must be at most 9.99, got 10.0"You can round to a given number of decimal places:
iex> Z.decimal(precision: 2)
iex> |> Z.parse(Decimal.new("3.14159"))
{:ok, Decimal.new("3.14")}It can be coerced from an int:
iex> Z.decimal()
iex> |> Z.parse(42, coerce: true)
{:ok, Decimal.new("42")}It can be coerced from a float:
iex> Z.decimal()
iex> |> Z.parse(3.14, coerce: true)
{:ok, Decimal.new("3.14")}It can be coerced from a string:
iex> Z.decimal()
iex> |> Z.parse("3.14", coerce: true)
{:ok, Decimal.new("3.14")}It can be converted into json schema:
iex> Z.decimal(min: 1.00, max: 100.00)
iex> |> Z.describe("A monetary amount.")
iex> |> Z.example(Decimal.new("19.99"))
iex> |> Z.json_schema()
%{
"type" => "number",
"description" => "A monetary amount.",
"examples" => [19.99],
"minimum" => 1.0,
"maximum" => 100.0
}
Sets the field as not-required and provides a default value.
Attaches a description to the type, for use in JSON Schema.
Creates a discriminated union of two or more map types.
Unlike union/1, this provides more precise error reporting by
using a discriminator field to determine which variant to parse.
Examples
Successful parsing with different variants:
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.literal("dog"), barks: Z.boolean()}),
iex> Z.map(%{type: Z.literal("cat"), meows: Z.boolean()})
iex> ])
iex> |> Z.parse(%{type: "dog", barks: true})
{:ok, %{type: "dog", barks: true}}
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.literal("dog"), barks: Z.boolean()}),
iex> Z.map(%{type: Z.literal("cat"), meows: Z.boolean()})
iex> ])
iex> |> Z.parse(%{type: "cat", meows: true})
{:ok, %{type: "cat", meows: true}}Works with string keys in the input:
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.literal("dog"), barks: Z.boolean()}),
iex> Z.map(%{type: Z.literal("cat"), meows: Z.boolean()})
iex> ])
iex> |> Z.parse(%{"type" => "dog", "barks" => true})
{:ok, %{type: "dog", barks: true}}Error when discriminator value doesn't match any variant:
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.literal("dog"), barks: Z.boolean()}),
iex> Z.map(%{type: Z.literal("cat"), meows: Z.boolean()})
iex> ])
iex> |> Z.parse(%{type: "bird", flies: true})
iex> |> unwrap_issue_message()
"expected field type to be one of 'dog' or 'cat', got 'bird'"Error when input is not a map:
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.literal("dog"), barks: Z.boolean()}),
iex> Z.map(%{type: Z.literal("cat"), meows: Z.boolean()})
iex> ])
iex> |> Z.parse("not a map")
iex> |> unwrap_issue_message()
"expected type map, got string"ArgumentError when discriminator field is missing from a map type:
iex> try do
iex> Z.discriminated_union(:kind, [
iex> Z.map(%{type: Z.literal("dog")}),
iex> Z.map(%{type: Z.literal("cat")})
iex> ])
iex> rescue
iex> e in ArgumentError -> e.message
iex> end
"the discriminator field :kind must exist in all map types"ArgumentError when inner types are not map types:
iex> try do
iex> Z.discriminated_union(:type, [Z.string(), Z.integer()])
iex> rescue
iex> e in ArgumentError -> e.message
iex> end
"discriminated union only accepts map types, got Zot.Type.String"ArgumentError when discriminator field is not a literal type:
iex> try do
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.string(), name: Z.string()}),
iex> Z.map(%{type: Z.string(), age: Z.integer()})
iex> ])
iex> rescue
iex> e in ArgumentError -> e.message
iex> end
"the discriminator field :type must be a literal type, got Zot.Type.String"It can be converted into json schema:
iex> Z.discriminated_union(:type, [
iex> Z.map(%{type: Z.literal("dog"), barks: Z.boolean()}),
iex> Z.map(%{type: Z.literal("cat"), meows: Z.boolean()})
iex> ])
iex> |> Z.json_schema()
%{
"oneOf" => [
%{
"type" => "object",
"additionalProperties" => true,
"properties" => %{
"type" => %{"const" => "dog"},
"barks" => %{"type" => "boolean"}
},
"required" => ["type", "barks"]
},
%{
"type" => "object",
"additionalProperties" => true,
"properties" => %{
"type" => %{"const" => "cat"},
"meows" => %{"type" => "boolean"}
},
"required" => ["type", "meows"]
}
],
"discriminator" => %{
"propertyName" => "type"
}
}
Alias to date_time/1.
Creates an email type.
Examples
iex> Z.email()
iex> |> Z.parse("foo@zot.dev")
{:ok, "foo@zot.dev"}You can optionally specify a ruleset for validation:
iex> Z.email(ruleset: :html5)
iex> |> Z.parse("invalid-email")
iex> |> unwrap_issue_message()
"is invalid"
iex> Z.email(ruleset: :ref5322)
iex> |> Z.parse("invalid-email")
iex> |> unwrap_issue_message()
"is invalid"
iex> Z.email(ruleset: :unicode)
iex> |> Z.parse("invalid-email")
iex> |> unwrap_issue_message()
"is invalid"It can be converted into json schema:
iex> Z.email()
iex> |> Z.describe("A user's email address.")
iex> |> Z.example("foo@zot.dev")
iex> |> Z.json_schema()
%{
"type" => "string",
"format" => "email",
"description" => "A user's email address.",
"examples" => ["foo@zot.dev"]
}
Enforces that the string ends with the given substring.
Creates an enum type.
Examples
Values can be all atoms:
iex> Z.enum([:red, :green, :blue])
iex> |> Z.parse(:green)
{:ok, :green}
iex> Z.enum([:red, :green, :blue])
iex> |> Z.parse(:yellow)
iex> |> unwrap_issue_message()
"must be :red, :green or :blue, got :yellow"Or they can be all strings:
iex> Z.enum(["small", "medium", "large"])
iex> |> Z.parse("medium")
{:ok, "medium"}
iex> Z.enum(["small", "medium", "large"])
iex> |> Z.parse("extra large")
iex> |> unwrap_issue_message()
"must be 'small', 'medium' or 'large', got 'extra large'"It can be converted to json schema:
iex> Z.enum([:red, :green, :blue])
iex> |> Z.describe("A color.")
iex> |> Z.example(:green)
iex> |> Z.json_schema()
%{
"type" => "string",
"enum" => ["red", "green", "blue"],
"description" => "A color.",
"examples" => ["green"]
}
Attaches an example value to the type, for use in JSON Schema.
Creates a float type.
Examples
iex> Z.float()
iex> |> Z.parse(3.14)
{:ok, 3.14}You can enforce a minimum value:
iex> Z.float(min: 1.0)
iex> |> Z.parse(0.99)
iex> |> unwrap_issue_message()
"must be at least 1.0, got 0.99"You can enforce a maximum value:
iex> Z.float(max: 10.0)
iex> |> Z.parse(10.01)
iex> |> unwrap_issue_message()
"must be at most 10.0, got 10.01"You can round to a given number of decimal places:
iex> Z.float(precision: 2)
iex> |> Z.parse(3.14159)
{:ok, 3.14}It can be coerced from an int:
iex> Z.float()
iex> |> Z.parse(42, coerce: true)
{:ok, 42.0}It can be coerced from Decimal:
iex> Z.float()
iex> |> Z.parse(Decimal.new("3.14"), coerce: true)
{:ok, 3.14}It can be coerced from a string:
iex> Z.float()
iex> |> Z.parse("3.14", coerce: true)
{:ok, 3.14}It can be converted into json schema:
iex> Z.float(min: 0.0, max: 1.0)
iex> |> Z.describe("A percentage.")
iex> |> Z.example(0.425)
iex> |> Z.json_schema()
%{
"type" => "number",
"description" => "A percentage.",
"examples" => [0.425],
"minimum" => 0.0,
"maximum" => 1.0
}
Restricts which ports are forbidden in a URL.
See url/1 for more details.
Alias to integer/1.
Creates a integer type.
Examples
iex> Z.integer()
iex> |> Z.parse(42)
{:ok, 42}You can enforce a minimum value:
iex> Z.integer(min: 18)
iex> |> Z.parse(16)
iex> |> unwrap_issue_message()
"must be at least 18, got 16"You can enforce a maximum value:
iex> Z.integer(max: 18)
iex> |> Z.parse(33)
iex> |> unwrap_issue_message()
"must be at most 18, got 33"It can be coerced from an float (rounded):
iex> Z.integer()
iex> |> Z.parse(3.14, coerce: true)
{:ok, 3}It can be coerced from Decimal (rounded):
iex> Z.integer()
iex> |> Z.parse(Decimal.new("3.14"), coerce: true)
{:ok, 3}It can be coerced from a string:
iex> Z.integer()
iex> |> Z.parse("42", coerce: true)
{:ok, 42}It can be converted into json schema:
iex> Z.integer(min: 0, max: 100)
iex> |> Z.describe("A percentage.")
iex> |> Z.example(42)
iex> |> Z.json_schema()
%{
"type" => "integer",
"description" => "A percentage.",
"examples" => [42],
"minimum" => 0,
"maximum" => 100
}
Creates an IP address type.
Examples
iex> Z.ip()
iex> |> Z.parse("192.168.1.1")
{:ok, "192.168.1.1"}
iex> Z.ip()
iex> |> Z.parse("::1")
{:ok, "::1"}
iex> Z.ip()
iex> |> Z.parse("not-an-ip")
iex> |> unwrap_issue_message()
"is invalid"You can restrict to a specific IP version:
iex> Z.ip(version: :v4)
iex> |> Z.parse("192.168.1.1")
{:ok, "192.168.1.1"}
iex> Z.ip(version: :v4)
iex> |> Z.parse("::1")
iex> |> unwrap_issue_message()
"must be a valid IPv4 address"
iex> Z.ip(version: :v6)
iex> |> Z.parse("::1")
{:ok, "::1"}
iex> Z.ip(version: :v6)
iex> |> Z.parse("192.168.1.1")
iex> |> unwrap_issue_message()
"must be a valid IPv6 address"You can change the output format to a tuple:
iex> Z.ip(output: :tuple)
iex> |> Z.parse("192.168.1.1")
{:ok, {192, 168, 1, 1}}
iex> Z.ip(output: :tuple)
iex> |> Z.parse("::1")
{:ok, {0, 0, 0, 0, 0, 0, 0, 1}}It supports coercion from tuples:
iex> Z.ip()
iex> |> Z.parse({192, 168, 1, 1}, coerce: true)
{:ok, "192.168.1.1"}
iex> Z.ip()
iex> |> Z.parse({0, 0, 0, 0, 0, 0, 0, 1}, coerce: true)
{:ok, "::1"}You can validate against CIDR ranges:
iex> Z.ip()
iex> |> Z.cidr("192.168.0.0/16")
iex> |> Z.parse("192.168.1.1")
{:ok, "192.168.1.1"}
iex> Z.ip()
iex> |> Z.cidr("192.168.0.0/16")
iex> |> Z.parse("10.0.0.1")
iex> |> unwrap_issue_message()
"must be within CIDR range 192.168.0.0/16"You can use predefined CIDR sets:
iex> Z.ip()
iex> |> Z.cidr(:private)
iex> |> Z.parse("192.168.1.1")
{:ok, "192.168.1.1"}
iex> Z.ip()
iex> |> Z.cidr(:private)
iex> |> Z.parse("8.8.8.8")
iex> |> unwrap_issue_message()
"must be a private IP address"
iex> Z.ip()
iex> |> Z.cidr(:loopback)
iex> |> Z.parse("127.0.0.1")
{:ok, "127.0.0.1"}
iex> Z.ip()
iex> |> Z.cidr(:link_local)
iex> |> Z.parse("169.254.1.1")
{:ok, "169.254.1.1"}It can be converted into json schema:
iex> Z.ip(version: :v4)
iex> |> Z.describe("An IPv4 address.")
iex> |> Z.example("192.168.1.1")
iex> |> Z.json_schema()
%{
"type" => "string",
"format" => "ipv4",
"description" => "An IPv4 address.",
"examples" => ["192.168.1.1"]
}
iex> Z.ip(version: :v6)
iex> |> Z.describe("An IPv6 address.")
iex> |> Z.example("::1")
iex> |> Z.json_schema()
%{
"type" => "string",
"format" => "ipv6",
"description" => "An IPv6 address.",
"examples" => ["::1"]
}
iex> Z.ip()
iex> |> Z.describe("An IP address.")
iex> |> Z.json_schema()
%{
"description" => "An IP address.",
"oneOf" => [
%{"format" => "ipv4", "type" => "string"},
%{"format" => "ipv6", "type" => "string"}
]
}
@spec json_schema(type) :: map() when type: Zot.Type.t()
Converts the given type into a JSON Schema.
Defines the behavior for leading plus signs in phone numbers.
See phone/1 for more details.
Enforces that the string or list has the given length.
Creates a list type.
Examples
iex> Z.string()
iex> |> Z.list()
iex> |> Z.parse(["hello", "world"])
{:ok, ["hello", "world"]}You can enforce a minimum length:
iex> Z.string()
iex> |> Z.list(min: 3)
iex> |> Z.parse(["one", "two"])
iex> |> unwrap_issue_message()
"must have at least 3 items, got 2"You can enforce a maximum length:
iex> Z.string()
iex> |> Z.list(max: 2)
iex> |> Z.parse(["one", "two", "three"])
iex> |> unwrap_issue_message()
"must have at most 2 items, got 3"You can enforce an exact length:
iex> Z.string()
iex> |> Z.list(length: 2)
iex> |> Z.parse(["one", "two", "three"])
iex> |> unwrap_issue_message()
"must have 2 items, got 3"It can be converted into json schema:
iex> Z.string()
iex> |> Z.list(min: 1, max: 5)
iex> |> Z.describe("A list of tags.")
iex> |> Z.example(["elixir", "zot"])
iex> |> Z.json_schema()
%{
"type" => "array",
"items" => %{
"type" => "string"
},
"description" => "A list of tags.",
"minItems" => 1,
"maxItems" => 5
}
Creates a literal type.
Examples
It can be a boolean:
iex> Z.literal(true)
iex> |> Z.parse(true)
{:ok, true}
iex> Z.literal(true)
iex> |> Z.parse(false)
iex> |> unwrap_issue_message()
"must be true, got false"
iex> Z.literal(true)
iex> |> Z.parse("enabled", coerce: true)
{:ok, true}It can be an integer:
iex> Z.literal(42)
iex> |> Z.parse(42)
{:ok, 42}
iex> Z.literal(42)
iex> |> Z.parse(43)
iex> |> unwrap_issue_message()
"must be 42, got 43"
iex> Z.literal(42)
iex> |> Z.parse("42", coerce: true)
{:ok, 42}It can be a float:
iex> Z.literal(3.14)
iex> |> Z.parse(3.14)
{:ok, 3.14}
iex> Z.literal(3.14)
iex> |> Z.parse(3.13)
iex> |> unwrap_issue_message()
"must be 3.14, got 3.13"
iex> Z.literal(3.14)
iex> |> Z.parse("3.14", coerce: true)
{:ok, 3.14}It can be a string:
iex> Z.literal("foo")
iex> |> Z.parse("foo")
{:ok, "foo"}
iex> Z.literal("foo")
iex> |> Z.parse("bar")
iex> |> unwrap_issue_message()
"must be 'foo', got 'bar'"It can be an atom:
iex> Z.literal(:admin)
iex> |> Z.parse(:admin)
{:ok, :admin}
iex> Z.literal(:admin)
iex> |> Z.parse(:user)
iex> |> unwrap_issue_message()
"must be :admin, got :user"
iex> Z.literal(:admin)
iex> |> Z.parse("admin", coerce: true)
{:ok, :admin}
iex> Z.literal(:admin)
iex> |> Z.parse("user", coerce: true)
iex> |> unwrap_issue_message()
"must be :admin, got 'user'"It can be converted into json schema:
iex> Z.literal("active")
iex> |> Z.describe("Lorem ipsum.")
iex> |> Z.json_schema()
%{
"const" => "active",
"description" => "Lorem ipsum.",
}
Creates a map type where unknown fields are stripped out.
Examples
iex> Z.map(%{name: Z.string(), age: Z.integer(min: 18)})
iex> |> Z.parse(%{name: "Alice", age: 18, email: "alice@wonder.land"})
{:ok, %{name: "Alice", age: 18}}
iex> {:error, [issue]} =
iex> Z.map(%{name: Z.string(), age: Z.integer(min: 18)})
iex> |> Z.parse(%{name: "Alice", age: 16, email: "alice@wonder.land"})
iex>
iex> assert issue.path == [:age]
iex> assert Exception.message(issue) == "must be at least 18, got 16"It can be converted into json schema:
iex> Z.map(%{name: Z.string(), age: Z.integer(min: 0)})
iex> |> Z.describe("A person's profile.")
iex> |> Z.example(%{name: "Bob", age: 30})
iex> |> Z.json_schema()
%{
"type" => "object",
"description" => "A person's profile.",
"examples" => [%{"name" => "Bob", "age" => 30}],
"properties" => %{
"name" => %{
"type" => "string"
},
"age" => %{
"type" => "integer",
"minimum" => 0
}
},
"required" => ["name", "age"],
"additionalProperties" => true
}
Enforces a maximum value for the given type.
Enforces a maximum prefix length for CIDR notation.
See cidr/1 for more details.
Merges two map types into a new map type.
The second map's fields override the first on conflicts. The resulting map is strict if either input map is strict.
Note that required, default, description, example, and effects
are lost when merging two maps. Use the appropriate modifiers after
merging to set these fields.
Examples
iex> map1 = Z.map(%{name: Z.string()})
iex> map2 = Z.map(%{age: Z.integer()})
iex> Z.merge(map1, map2)
iex> |> Z.parse(%{name: "Alice", age: 30})
{:ok, %{name: "Alice", age: 30}}
iex> map1 = Z.map(%{name: Z.string()})
iex> map2 = Z.map(%{name: Z.integer()})
iex> Z.merge(map1, map2)
iex> |> Z.parse(%{name: 42})
{:ok, %{name: 42}}
iex> map1 = Z.strict_map(%{name: Z.string()})
iex> map2 = Z.map(%{age: Z.integer()})
iex> Z.merge(map1, map2)
iex> |> Z.parse(%{name: "Alice", age: 30, extra: "field"})
iex> |> unwrap_issue_message()
"unknown field"
Enforces a minimum value for the given type.
Enforces a minimum prefix length for CIDR notation.
See cidr/1 for more details.
Creates a non-empty string type that trims whitespace.
This is an alias for string(trim: true, min: 1).
Examples
iex> Z.non_empty_string()
iex> |> Z.parse("hello")
{:ok, "hello"}
iex> Z.non_empty_string()
iex> |> Z.parse(" ")
iex> |> unwrap_issue_message()
"must be at least 1 characters long, got 0"
iex> Z.non_empty_string()
iex> |> Z.parse("")
iex> |> unwrap_issue_message()
"must be at least 1 characters long, got 0"Whitespace is trimmed before validation:
iex> Z.non_empty_string()
iex> |> Z.parse(" hello ")
{:ok, "hello"}
Creates a number type (union of integer and float types).
Examples
iex> Z.number()
iex> |> Z.parse(3.14)
{:ok, 3.14}
iex> Z.number()
iex> |> Z.parse(42)
{:ok, 42}It can be converted into json schema:
iex> Z.number(min: 0.5, max: 100)
iex> |> Z.describe("A percentage.")
iex> |> Z.example(42)
iex> |> Z.json_schema()
%{
"type" => "number",
"description" => "A percentage.",
"examples" => [42],
"minimum" => 0.5,
"maximum" => 100
}
Creates a numeric string type.
Examples
iex> Z.numeric()
iex> |> Z.parse("123456")
{:ok, "123456"}
iex> Z.numeric()
iex> |> Z.parse("123abc")
iex> |> unwrap_issue_message()
"must contain only 0-9 digits"You can enforce a minimum length:
iex> Z.numeric(min: 5)
iex> |> Z.parse("1234")
iex> |> unwrap_issue_message()
"must be at least 5 characters long, got 4"You can enforce a maximum length:
iex> Z.numeric(max: 10)
iex> |> Z.parse("12345678901")
iex> |> unwrap_issue_message()
"must be at most 10 characters long, got 11"It can be converted into json schema:
iex> Z.numeric(min: 3, max: 8)
iex> |> Z.describe("A numeric code.")
iex> |> Z.example("123456")
iex> |> Z.json_schema()
%{
"type" => "string",
"description" => "A numeric code.",
"examples" => ["123456"],
"pattern" => "^[0-9]+$",
"minLength" => 3,
"maxLength" => 8
}
Creates a new map type excluding the specified keys from the original shape.
Examples
iex> Z.strict_map(%{id: Z.uuid(), name: Z.string(), email: Z.email()})
iex> |> Z.omit([:email])
iex> |> Z.parse(%{id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice"})
{:ok, %{id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice"}}
iex> {:error, [issue]} =
iex> Z.strict_map(%{id: Z.uuid(), name: Z.string(), password: Z.string()})
iex> |> Z.omit([:password])
iex> |> Z.parse(%{id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice", password: "secret"})
iex>
iex> assert issue.path == ["password"]
iex> assert Exception.message(issue) == "unknown field"
Sets the field as not required (nullable).
Sets the output format for the given type.
For IP types, accepts :string (default) or :tuple.
For CIDR types, accepts :string (default), :tuple, or :map.
@spec parse(type, input, [option]) :: {:ok, output} | {:error, [Zot.Issue.t(), ...]} when type: Zot.Type.t(), input: term(), option: {:coerce, boolean() | :unsafe}, output: term()
Parses the given input according to the given type.
Makes all fields optional. Optionally drops all nil fields from the resulting map, after successfully parsed and validate.
Examples
iex> Z.strict_map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.partial()
iex> |> Z.parse(%{name: "Alice"})
{:ok, %{name: "Alice", age: nil}}
iex> Z.strict_map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.partial()
iex> |> Z.parse(%{})
{:ok, %{name: nil, age: nil}}You can optionally compact the resulting map (drop nil fields):
iex> Z.strict_map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.partial(compact: true)
iex> |> Z.parse(%{name: "Alice"})
{:ok, %{name: "Alice"}}It can be converted into json schema:
iex> Z.strict_map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.partial()
iex> |> Z.describe("A person's profile.")
iex> |> Z.example(%{"name" => "Bob", "age" => 18})
iex> |> Z.json_schema()
%{
"type" => "object",
"description" => "A person's profile.",
"examples" => [%{"name" => "Bob", "age" => 18}],
"properties" => %{
"name" => %{"type" => ["string", nil]},
"age" => %{"type" => ["integer", nil]}
},
"required" => [],
"additionalProperties" => false
}
Alias for partial/2 with option compact: true.
Creates a phone number type.
Examples
iex> Z.phone()
iex> |> Z.parse("+5511987654321")
{:ok, "+5511987654321"}
iex> Z.phone()
iex> |> Z.parse("5511987654321")
{:ok, "5511987654321"}You can define the behavior for the leading plus sign, where the options are:
:always- if absent, adds it to the output;:keep(default) - if present, keeps it;:never- if present, results in an issue;:require- if absent, results in an issue; or:trim- if present, removes it from the output.iex> Z.phone(leading_plus_sign: :always) iex> |> Z.parse("5511987654321")
iex> Z.phone(leading_plus_sign: :keep) iex> |> Z.parse("+5511987654321")
iex> Z.phone(leading_plus_sign: :keep) iex> |> Z.parse("5511987654321")
iex> Z.phone(leading_plus_sign: :never) iex> |> Z.parse("+5511987654321") iex> |> unwrap_issue_message() "must be digits only, without the leading plus sign (+)"
iex> Z.phone(leading_plus_sign: :require) iex> |> Z.parse("5511987654321") iex> |> unwrap_issue_message() "must start with a leading plus sign (+)"
iex> Z.phone(leading_plus_sign: :trim) iex> |> Z.parse("+5511987654321")
iex> Z.phone(leading_plus_sign: :trim) iex> |> Z.parse("5511987654321")
It can be converted into json schema:
iex> Z.phone(leading_plus_sign: :always)
iex> |> Z.describe("A phone number.")
iex> |> Z.example("+5511987654321")
iex> |> Z.json_schema()
%{
"description" => "A phone number.",
"examples" => ["+5511987654321"],
"format" => "phone",
"maxLength" => 16,
"minLength" => 9,
"pattern" => "^\\+?[0-9]{8,15}$",
"type" => "string"
}
Creates a new map type with only the specified keys from the original shape.
Examples
iex> Z.strict_map(%{id: Z.uuid(), name: Z.string(), email: Z.email()})
iex> |> Z.pick([:id, :name])
iex> |> Z.parse(%{id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice"})
{:ok, %{id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice"}}
iex> {:error, [issue]} =
iex> Z.strict_map(%{id: Z.uuid(), name: Z.string(), email: Z.email()})
iex> |> Z.pick([:id, :name])
iex> |> Z.parse(%{id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice", email: "alice@example.com"})
iex>
iex> assert issue.path == ["email"]
iex> assert Exception.message(issue) == "unknown field"
Rounds the float to the given number of decimal places.
Examples
iex> Z.float()
iex> |> Z.precision(2)
iex> |> Z.parse(3.14159)
{:ok, 3.14}
iex> Z.decimal()
iex> |> Z.precision(2)
iex> |> Z.parse(Decimal.new("3.14159"))
{:ok, Decimal.new("3.14")}
Renders a list of issues into a pretty-printed string for display.
By default, the output includes ANSI escape codes for highlighting.
You can disable that by setting the option :colors to false.
Examples
iex> {:error, issues} =
iex> Z.map(%{name: Z.string(), age: Z.integer(min: 18)})
iex> |> Z.parse(%{age: 16})
iex>
iex> Z.pretty_print(issues, colors: false)
" * Field `age` must be at least 18, got 16\n * Field `name` is required"
Defines the behavior regarding query strings in URLs.
See url/1 for more details.
Sets both min and max from an Elixir Range.
Creates a record type where keys are non-empty strings.
Examples
iex> Z.record(Z.integer())
iex> |> Z.parse(%{"a" => 1, "b" => 2})
{:ok, %{"a" => 1, "b" => 2}}
iex> {:error, [issue]} =
iex> Z.record(Z.float())
iex> |> Z.parse(%{"a" => 3.14, "b" => "not a float"})
iex>
iex> assert issue.path == ["b"]
iex> assert Exception.message(issue) == "expected type float, got string"
Adds a custom refinement to the given type's effects pipeline, which is executed after the type is successfully parsed and validated.
Examples
iex> Z.integer()
iex> |> Z.refine(& &1 >= 18)
iex> |> Z.parse(16)
iex> |> unwrap_issue_message()
"is invalid"You can optionally provide a custom error message:
iex> Z.integer()
iex> |> Z.refine(& &1 >= 18, error: "must be greater than or equal to 18")
iex> |> Z.parse(16)
iex> |> unwrap_issue_message()
"must be greater than or equal to 18"The error message may include the actual value:
iex> Z.integer()
iex> |> Z.refine(& &1 >= 18, error: "must be greater than or equal to 18, got %{actual}")
iex> |> Z.parse(16)
iex> |> unwrap_issue_message()
"must be greater than or equal to 18, got 16"
Enforces that the string matches the given regex.
Requires that the URL has a non-root path.
See url/1 for more details.
Creates a set type (a list of unique items).
It works exactly like list/2, except that it always deduplicates
the output. You can enforce that the input itself must contain no
duplicates by using the unique option.
Examples
iex> Z.string()
iex> |> Z.set()
iex> |> Z.parse(["a", "b", "a"])
{:ok, ["a", "b"]}You can enforce that the input must have unique items:
iex> Z.string()
iex> |> Z.set(unique: :enforce)
iex> |> Z.parse(["a", "b", "a"])
iex> |> unwrap_issue_message()
"expected unique values only, found duplicate at index 2"You can enforce a minimum length:
iex> Z.string()
iex> |> Z.set(min: 2)
iex> |> Z.parse(["one"])
iex> |> unwrap_issue_message()
"must have at least 2 items, got 1"You can enforce a maximum length:
iex> Z.string()
iex> |> Z.set(max: 2)
iex> |> Z.parse(["one", "two", "three"])
iex> |> unwrap_issue_message()
"must have at most 2 items, got 3"It can be converted into json schema:
iex> Z.string()
iex> |> Z.set(min: 1, max: 5)
iex> |> Z.describe("A set of tags.")
iex> |> Z.example(["elixir", "zot"])
iex> |> Z.json_schema()
%{
"type" => "array",
"items" => %{
"type" => "string"
},
"description" => "A set of tags.",
"minItems" => 1,
"maxItems" => 5,
"uniqueItems" => true
}
Enforces that the string starts with the given substring.
Creates a map type where unknown fields cause an issue.
Examples
iex> Z.strict_map(%{name: Z.string(), age: Z.integer(min: 18)})
iex> |> Z.parse(%{name: "Alice", age: 18})
{:ok, %{name: "Alice", age: 18}}
iex> {:error, [issue]} =
iex> Z.strict_map(%{name: Z.string(), age: Z.integer(min: 18)})
iex> |> Z.parse(%{name: "Alice", age: 18, email: "alice@wonder.land"})
iex>
iex> assert issue.path == ["email"]
iex> assert Exception.message(issue) == "unknown field"It can be converted into json schema:
iex> Z.strict_map(%{name: Z.string(), age: Z.integer(min: 0)})
iex> |> Z.describe("A person's profile.")
iex> |> Z.example(%{name: "Bob", age: 30})
iex> |> Z.json_schema()
%{
"type" => "object",
"description" => "A person's profile.",
"examples" => [%{"name" => "Bob", "age" => 30}],
"properties" => %{
"name" => %{
"type" => "string"
},
"age" => %{
"type" => "integer",
"minimum" => 0
}
},
"required" => ["name", "age"],
"additionalProperties" => false
}
Creates a string type.
Examples
iex> Z.string()
iex> |> Z.parse("hello world")
{:ok, "hello world"}Can enforce that the string contains a given substring:
iex> Z.string(contains: "foo")
iex> |> Z.parse("bar baz")
iex> |> unwrap_issue_message()
"must contain 'foo'"
iex> Z.string()
iex> |> Z.contains("foo")
iex> |> Z.parse("bar baz")
iex> |> unwrap_issue_message()
"must contain 'foo'"Can enforce a string length:
iex> Z.string(length: 5)
iex> |> Z.parse("hey")
iex> |> unwrap_issue_message()
"must be 5 characters long, got 3"
iex> Z.string()
iex> |> Z.length(5)
iex> |> Z.parse("hey")
iex> |> unwrap_issue_message()
"must be 5 characters long, got 3"Can enforce a minimum string length:
iex> Z.string(min: 3)
iex> |> Z.parse("hi")
iex> |> unwrap_issue_message()
"must be at least 3 characters long, got 2"
iex> Z.string()
iex> |> Z.min(3)
iex> |> Z.parse("hi")
iex> |> unwrap_issue_message()
"must be at least 3 characters long, got 2"Can enforce a maximum string length:
iex> Z.string(max: 10)
iex> |> Z.parse("this is a very long string")
iex> |> unwrap_issue_message()
"must be at most 10 characters long, got 26"
iex> Z.string()
iex> |> Z.max(10)
iex> |> Z.parse("this is a very long string")
iex> |> unwrap_issue_message()
"must be at most 10 characters long, got 26"Can enforce that the string starts with a given substring:
iex> Z.string(starts_with: "Hello")
iex> |> Z.parse("World, Hello!")
iex> |> unwrap_issue_message()
"must start with 'Hello'"
iex> Z.string()
iex> |> Z.starts_with("Hello")
iex> |> Z.parse("World, Hello!")
iex> |> unwrap_issue_message()
"must start with 'Hello'"Can enforce that the string ends with a given substring:
iex> Z.string(ends_with: "World!")
iex> |> Z.parse("World, Hello!")
iex> |> unwrap_issue_message()
"must end with 'World!'"
iex> Z.string()
iex> |> Z.ends_with("World!")
iex> |> Z.parse("World, Hello!")
iex> |> unwrap_issue_message()
"must end with 'World!'"Can enforce that the string matches a given regex:
iex> Z.string(regex: ~r/^hello/)
iex> |> Z.parse("world hello")
iex> |> unwrap_issue_message()
"must match pattern /^hello/"
iex> Z.string()
iex> |> Z.regex(~r/^hello/)
iex> |> Z.parse("world hello")
iex> |> unwrap_issue_message()
"must match pattern /^hello/"You can specify for the string to be trimmed before validation:DSS
iex> Z.string(trim: true, starts_with: "Hello")
iex> |> Z.parse(" Hello, World!")
{:ok, "Hello, World!"}
iex> Z.string()
iex> |> Z.trim()
iex> |> Z.starts_with("Hello")
iex> |> Z.parse(" Hello, World!")
{:ok, "Hello, World!"}It can be converted into json schema:
iex> Z.string(starts_with: "u_", length: 28)
iex> |> Z.describe("A user id.")
iex> |> Z.example("u_12345678901234567890123456")
iex> |> Z.json_schema()
%{
"type" => "string",
"description" => "A user id.",
"examples" => ["u_12345678901234567890123456"],
"minLength" => 28,
"maxLength" => 28
}
Creates a struct type.
It works like strict_map/1 but converts the result to an Elixir
struct.
Examples
iex> Z.struct(ZotTest.StructUser, %{name: Z.string(), age: Z.integer()})
iex> |> Z.parse(%{name: "Alice", age: 30})
{:ok, %ZotTest.StructUser{name: "Alice", age: 30}}Also accepts keyword list for shape (like map/1):
iex> Z.struct(ZotTest.StructUser, name: Z.string(), age: Z.integer())
iex> |> Z.parse(%{"name" => "Bob", "age" => 25})
{:ok, %ZotTest.StructUser{name: "Bob", age: 25}}Rejects unknown fields (strict mode behavior):
iex> Z.struct(ZotTest.StructUser, %{name: Z.string(), age: Z.integer()})
iex> |> Z.parse(%{name: "Alice", age: 30, email: "alice@example.com"})
iex> |> unwrap_issue_message()
"unknown field"Returns validation errors for invalid field values:
iex> Z.struct(ZotTest.StructUser, %{name: Z.string(), age: Z.integer(min: 18)})
iex> |> Z.parse(%{name: "Alice", age: 16})
iex> |> unwrap_issue_message()
"must be at least 18, got 16"Works with coercion:
iex> Z.struct(ZotTest.StructUser, %{name: Z.string(), age: Z.integer()})
iex> |> Z.parse(%{name: "Alice", age: "30"}, coerce: true)
{:ok, %ZotTest.StructUser{name: "Alice", age: 30}}Alternatively, you can convert a map type into a struct type:DSS
iex> Z.strict_map(%{name: Z.string(), age: Z.integer()})
iex> |> Z.struct(ZotTest.StructUser)
iex> |> Z.parse(%{name: "Bob", age: 25})
{:ok, %ZotTest.StructUser{name: "Bob", age: 25}}It can be converted into json schema (same as strict_map):
iex> Z.struct(ZotTest.StructUser, %{name: Z.string(), age: Z.integer(min: 0)})
iex> |> Z.describe("A user profile.")
iex> |> Z.json_schema()
%{
"type" => "object",
"description" => "A user profile.",
"properties" => %{
"name" => %{
"type" => "string"
},
"age" => %{
"type" => "integer",
"minimum" => 0
}
},
"required" => ["name", "age"],
"additionalProperties" => false
}
Summarizes a list of issues into a map of paths (dot-notated) to messages.
Examples
iex> {:error, issues} =
iex> Z.map(%{user: Z.map(%{name: Z.string(), age: Z.integer(min: 18)})})
iex> |> Z.parse(%{user: %{name: 123, age: 16}})
iex>
iex> Z.summarize(issues)
%{
"user.name" => ["expected type string, got integer"],
"user.age" => ["must be at least 18, got 16"]
}
iex> {:error, issues} =
iex> Z.map(%{email: Z.email()})
iex> |> Z.parse(%{email: "invalid"})
iex>
iex> Z.summarize(issues)
%{"email" => ["is invalid"]}
Alias to date_time/1.
Defines the behavior regarding trailing slashes in URLs.
See url/1 for more details.
Adds a transformation to the given type's effects pipeline, which is executed after the type is successfully parsed and validated.
Examples
iex> Z.integer()
iex> |> Z.transform(&Decimal.new/1)
iex> |> Z.parse(42)
{:ok, Decimal.new(42)}
Trims whitespace from the beginning and end of the string before validation.
Alias to date_time/1.
Creates a tuple type with a fixed number of heterogeneous elements.
Examples
The argument can be a list of types:
iex> Z.tuple([Z.string(), Z.integer()])
iex> |> Z.parse({"hello", 42})
{:ok, {"hello", 42}}Or a tuple of types:
iex> Z.tuple({Z.string(), Z.integer()})
iex> |> Z.parse({"hello", 42})
{:ok, {"hello", 42}}Rejects tuples with wrong number of elements:
iex> Z.tuple([Z.string(), Z.integer()])
iex> |> Z.parse({"hello", 42, "extra"})
iex> |> unwrap_issue_message()
"expected a tuple with 2 elements, got 3"Validates each element against its corresponding type:
iex> Z.tuple([Z.string(), Z.integer()])
iex> |> Z.parse({"hello", "not an int"})
iex> |> unwrap_issue_message()
"expected type integer, got string"It can be coerced from a list:
iex> Z.tuple([Z.string(), Z.integer()])
iex> |> Z.parse(["hello", 42], coerce: true)
{:ok, {"hello", 42}}It can be converted into json schema:
iex> Z.tuple([Z.string(), Z.integer()])
iex> |> Z.describe("A name and age pair.")
iex> |> Z.example({"Alice", 30})
iex> |> Z.json_schema()
%{
"type" => "array",
"description" => "A name and age pair.",
"examples" => [["Alice", 30]],
"prefixItems" => [
%{"type" => "string"},
%{"type" => "integer"}
],
"items" => false,
"minItems" => 2,
"maxItems" => 2
}
Unwraps a branded type, returning its inner type.
Examples
iex> Z.string()
iex> |> Z.branded(:name)
iex> |> Z.unbranded()
iex> |> Z.parse("Rafael")
{:ok, "Rafael"}
Creates a union of two or more types.
Examples
iex> Z.union([Z.string(), Z.integer()])
iex> |> Z.parse("hello")
{:ok, "hello"}
iex> Z.union([Z.string(), Z.integer()])
iex> |> Z.parse(42)
{:ok, 42}Beware that only one of the types will have its error reported:
iex> Z.union([Z.string(), Z.integer()])
iex> |> Z.parse(3.14)
iex> |> unwrap_issue_message()
"expected type integer, got float"See discriminated_union/2 which provides more precise error
reporting at the cost of requiring a discriminator field.
It can be converted into json schema:
iex> Z.union([Z.string(), Z.integer()])
iex> |> Z.json_schema()
%{
"anyOf" => [
%{
"type" => "string"
},
%{
"type" => "integer"
}
]
}
Creates a URL string type.
Examples
iex> Z.url()
iex> |> Z.parse("https://zot.dev")
{:ok, "https://zot.dev"}
iex> Z.url()
iex> |> Z.parse("not a uri")
iex> |> unwrap_issue_message()
"is invalid"A host is always required:
iex> Z.url()
iex> |> Z.parse("/relative/path")
iex> |> unwrap_issue_message()
"host is required"
iex> Z.url()
iex> |> Z.parse("urn:isbn:0451450523")
iex> |> unwrap_issue_message()
"host is required"You can forbid loopback addresses (localhost, *.localhost,
127.x.x.x, ::1):
iex> Z.url(allow_loopback: false)
iex> |> Z.parse("https://localhost/path")
iex> |> unwrap_issue_message()
"loopback addresses are not allowed"
iex> Z.url(allow_loopback: false)
iex> |> Z.parse("https://foo.localhost/path")
iex> |> unwrap_issue_message()
"loopback addresses are not allowed"
iex> Z.url(allow_loopback: false)
iex> |> Z.parse("https://127.0.0.1/path")
iex> |> unwrap_issue_message()
"loopback addresses are not allowed"
iex> Z.url(allow_loopback: false)
iex> |> Z.parse("https://[::1]/path")
iex> |> unwrap_issue_message()
"loopback addresses are not allowed"
iex> Z.url(allow_loopback: false)
iex> |> Z.parse("https://example.com/path")
{:ok, "https://example.com/path"}You can require a non-root path:
iex> Z.url(require_path: true)
iex> |> Z.parse("https://example.com/foo")
{:ok, "https://example.com/foo"}
iex> Z.url(require_path: true)
iex> |> Z.parse("https://example.com")
iex> |> unwrap_issue_message()
"path is required"
iex> Z.url(require_path: true)
iex> |> Z.parse("https://example.com/")
iex> |> unwrap_issue_message()
"path is required"You can restrict which ports are allowed or forbidden:
iex> Z.url(allowed_ports: [80, 8080])
iex> |> Z.parse("https://example.com:8080/path")
{:ok, "https://example.com:8080/path"}
iex> Z.url(allowed_ports: [80, 443])
iex> |> Z.parse("https://example.com:9090/path")
iex> |> unwrap_issue_message()
"port must be 80 or 443, got 9090"
iex> Z.url(forbidden_ports: [25])
iex> |> Z.parse("https://example.com:25/path")
iex> |> unwrap_issue_message()
"port 25 is not allowed"
iex> Z.url(allowed_ports: [80, 443])
iex> |> Z.parse("https://example.com")
{:ok, "https://example.com"}You can enforce a limited set of allowed schemes:
iex> Z.url(allowed_schemes: ["http", "https"])
iex> |> Z.parse("ftp://zot.dev")
iex> |> unwrap_issue_message()
"scheme must be 'http' or 'https', got 'ftp'"You can specify whether query strings are forbidden, should be trimmed out from the URL, or kept (default):
iex> Z.url(query_string: :keep)
iex> |> Z.parse("https://zot.dev?page=1")
{:ok, "https://zot.dev?page=1"}
iex> Z.url(query_string: :forbid)
iex> |> Z.parse("https://zot.dev?page=1")
iex> |> unwrap_issue_message()
"query string is not allowed"
iex> Z.url(query_string: :trim)
iex> |> Z.parse("https://zot.dev?page=1")
{:ok, "https://zot.dev"}You can specify whether trailing slashes should always be present, should be kept if present (default), or should be trimmed out:
iex> Z.url(trailing_slash: :always)
iex> |> Z.parse("https://zot.dev/path")
{:ok, "https://zot.dev/path/"}
iex> Z.url(trailing_slash: :always)
iex> |> Z.parse("https://zot.dev/path/")
{:ok, "https://zot.dev/path/"}
iex> Z.url(trailing_slash: :trim)
iex> |> Z.parse("https://zot.dev/path/")
{:ok, "https://zot.dev/path"}
iex> Z.url(trailing_slash: :keep)
iex> |> Z.parse("https://zot.dev/path")
{:ok, "https://zot.dev/path"}
iex> Z.url(trailing_slash: :keep)
iex> |> Z.parse("https://zot.dev/path/")
{:ok, "https://zot.dev/path/"}
iex> Z.url(trailing_slash: :trim)
iex> |> Z.parse("https://zot.dev/path")
{:ok, "https://zot.dev/path"}
Creates a UUID type.
Examples
iex> Z.uuid()
iex> |> Z.parse("550e8400-e29b-41d4-a716-446655440000")
{:ok, "550e8400-e29b-41d4-a716-446655440000"}
iex> Z.uuid()
iex> |> Z.parse("not-a-uuid")
iex> |> unwrap_issue_message()
"is invalid"You can specify the UUID version to enforce:
iex> Z.uuid(:v1)
iex> |> Z.parse("550e8400-e29b-21d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v1, got v2"
iex> Z.uuid(:v2)
iex> |> Z.parse("550e8400-e29b-31d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v2, got v3"
iex> Z.uuid(:v3)
iex> |> Z.parse("550e8400-e29b-41d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v3, got v4"
iex> Z.uuid(:v4)
iex> |> Z.parse("550e8400-e29b-51d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v4, got v5"
iex> Z.uuid(:v5)
iex> |> Z.parse("550e8400-e29b-61d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v5, got v6"
iex> Z.uuid(:v6)
iex> |> Z.parse("550e8400-e29b-71d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v6, got v7"
iex> Z.uuid(:v7)
iex> |> Z.parse("550e8400-e29b-81d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v7, got v8"
iex> Z.uuid(:v8)
iex> |> Z.parse("550e8400-e29b-11d4-a716-446655440000")
iex> |> unwrap_issue_message()
"expected a uuid v8, got v1"It can be converted into json schema:
iex> Z.uuid(:v4)
iex> |> Z.describe("A universally unique identifier.")
iex> |> Z.example("550e8400-e29b-41d4-a716-446655440000")
iex> |> Z.json_schema()
%{
"type" => "string",
"format" => "uuid",
"description" => "A universally unique identifier.",
"examples" => ["550e8400-e29b-41d4-a716-446655440000"]
}
Enforces the version for the given type.
For CIDR types, accepts :any, :v4, or :v6.
For IP types, accepts :any, :v4, or :v6.
For UUID types, accepts :any, :v1 through :v8.
Pattern matches a Zot type struct.
Examples
Matching on any Zot type:
iex> zot_type(_) = Z.string()
Z.string()
iex> assert_raise MatchError, fn -> zot_type(_) = not_a_zot_type() endMatching on a specific Zot type:
iex> zot_type(Zot.Type.String) = Z.string()
Z.string()
iex> assert_raise MatchError, fn -> zot_type(Zot.Type.String) = not_a_zot_string() endMatching on any Zot type and binding its module to a variable:
iex> zot_type(mod) = Z.string()
iex> mod
Zot.Type.String