zz (zz v0.2.1)

View Source

Zod-like parsing and validation for Erlang.

Each combinator returns a parser/1 (or optional_parser/1 from optional/1). Compose them, then run with parse/2:

Z = zz:map(#{name => zz:binary(), age => zz:integer(#{min => 0})}),
{ok, User} = zz:parse(Z, #{name => <<"x">>, age => 1}),
<<"x">> = maps:get(name, User).

On failure, the nested errors/0 shape can be flattened to a path-addressed list of issues with issues/1, or rendered to a human-readable binary with format_issues/1.

Summary

Functions

Accept any input. Output equals input.

Validate that input is an atom.

Equivalent to binary/1.

Validate that input is a binary, with optional min/max byte size and regex constraints.

Equivalent to bitstring/1.

Validate that input is a bitstring, with optional min/max bit_size/1 constraints.

Validate that input is a boolean.

Validate that input is a single Unicode codepoint (char/0), an integer in 0..16#10FFFF.

Validate that input is a list of Unicode codepoints ([char()]), i.e. the old-style Erlang string representation. Element errors are wrapped as {list, Index, [not_char]}.

Validate that input equals (=:=) one of Values. Fails with not_in_enum if no value matches. Equivalent to a union/1 of literal/1s but with a flat error code.

Equivalent to float/1.

Validate that input is a float, with optional min/max.

Format issues/0 as a human-readable binary, one issue per line in the form path: code [extras]. Empty paths render as (root). Useful for logs and human-facing error output.

Validate that input is a function (any arity).

Validate that input is a function with the given arity.

Equivalent to integer/1.

Validate that input is an integer, with optional min/max.

Validate that input is iodata() (a binary or iolist()). Validation walks the entire structure via iolist_size/1, so cost is linear in the total bytes addressed.

Validate that input is an iolist() (a possibly-improper list of bytes, binaries, and nested iolists). Use iodata/0 if a raw binary should also be accepted.

Flatten nested errors/0 into a flat list of issue/0 records, each with a path to the failing position and a code.

Defer construction of a parser until parse time. Use to build self-referential (recursive) schemas without infinite recursion at definition time.

Validate that input is a list (any contents).

Equivalent to list/2.

Validate a homogeneous list, parsing each element with Z. Optional min/max constrain length.

Validate that input equals Value exactly (=:=).

Validate that input is a map (passthrough on contents).

Equivalent to map/2.

Validate a map against Schema. unknown_keys controls handling of keys not in Schema: strip (drop, default), passthrough (keep), strict (error).

Validate a homogeneous map where every key is parsed by KZ and every value by VZ. Use this for caches, dictionaries, and other arbitrary- keyed maps where the key shape is uniform.

Validate that input is a negative integer (=< -1).

Validate that input is a non-negative integer (>= 0).

Validate that input is undefined or matches Z. Sugar for union([literal(undefined), Z]).

Validate that input is a number (integer or float).

Mark a parser as optional in a schema/0. Inside a map/2 schema, an optional key may be absent without producing an error.

Run parser Z against Input.

Validate that input is a process identifier.

Validate that input is a positive integer (>= 1).

Validate that input is a reference (e.g. from make_ref/0).

Validate that input is a tuple (passthrough on contents).

Validate a fixed-arity tuple where each element is parsed by the corresponding parser at the same position in Zs. Element errors are wrapped as {tuple, Index, InnerErrors} with 1-based Index.

Validate against the first parser that succeeds. If none match, returns {error, [{no_match, [Errors1, Errors2, ...]}]} where each entry is the errors list from the corresponding parser, in input order. Empty union yields {error, [{no_match, []}]}.

Types

binary_options()

-type binary_options() :: #{min => non_neg_integer(), max => non_neg_integer(), regex => iodata()}.

bitstring_options()

-type bitstring_options() :: #{min => non_neg_integer(), max => non_neg_integer()}.

error()

-type error() ::
          atom() |
          {list, pos_integer(), errors()} |
          {tuple, pos_integer(), errors()} |
          {map_key, term(), errors()} |
          {map_value, term(), errors()} |
          {map_missing, term()} |
          {unknown_keys, [term()]} |
          {no_match, [errors()]}.

errors()

-type errors() :: [error()].

float_options()

-type float_options() :: #{min => float(), max => float()}.

integer_options()

-type integer_options() :: #{min => integer(), max => integer()}.

issue()

-type issue() :: #{path := path(), code := atom(), _ => _}.

issues()

-type issues() :: [issue()].

list_options()

-type list_options() :: #{min => non_neg_integer(), max => non_neg_integer()}.

map_options()

-type map_options() :: #{unknown_keys => strip | passthrough | strict}.

optional_parser()

-type optional_parser() :: optional_parser(term()).

optional_parser(T)

-type optional_parser(T) :: {optional, parser(T)}.

parser()

-type parser() :: parser(term()).

parser(T)

-type parser(T) :: fun((term()) -> result(T)).

path()

-type path() :: [term()].

result(T)

-type result(T) :: {ok, T} | {error, errors()}.

schema()

-type schema() :: #{term() => parser() | optional_parser()}.

Functions

any()

-spec any() -> parser(term()).

Accept any input. Output equals input.

atom()

-spec atom() -> parser(atom()).

Validate that input is an atom.

binary()

-spec binary() -> parser(binary()).

Equivalent to binary/1.

binary(Options)

-spec binary(binary_options()) -> parser(binary()).

Validate that input is a binary, with optional min/max byte size and regex constraints.

bitstring()

-spec bitstring() -> parser(bitstring()).

Equivalent to bitstring/1.

bitstring(Options)

-spec bitstring(bitstring_options()) -> parser(bitstring()).

Validate that input is a bitstring, with optional min/max bit_size/1 constraints.

boolean()

-spec boolean() -> parser(boolean()).

Validate that input is a boolean.

char()

-spec char() -> parser(char()).

Validate that input is a single Unicode codepoint (char/0), an integer in 0..16#10FFFF.

char_list()

-spec char_list() -> parser([char()]).

Validate that input is a list of Unicode codepoints ([char()]), i.e. the old-style Erlang string representation. Element errors are wrapped as {list, Index, [not_char]}.

enum(Values)

-spec enum([T]) -> parser(T).

Validate that input equals (=:=) one of Values. Fails with not_in_enum if no value matches. Equivalent to a union/1 of literal/1s but with a flat error code.

float()

-spec float() -> parser(float()).

Equivalent to float/1.

float(Options)

-spec float(float_options()) -> parser(float()).

Validate that input is a float, with optional min/max.

format_issues(Issues)

-spec format_issues(issues()) -> binary().

Format issues/0 as a human-readable binary, one issue per line in the form path: code [extras]. Empty paths render as (root). Useful for logs and human-facing error output.

function()

-spec function() -> parser(fun()).

Validate that input is a function (any arity).

function(Arity)

-spec function(arity()) -> parser(fun()).

Validate that input is a function with the given arity.

integer()

-spec integer() -> parser(integer()).

Equivalent to integer/1.

integer(Options)

-spec integer(integer_options()) -> parser(integer()).

Validate that input is an integer, with optional min/max.

iodata()

-spec iodata() -> parser(iodata()).

Validate that input is iodata() (a binary or iolist()). Validation walks the entire structure via iolist_size/1, so cost is linear in the total bytes addressed.

iolist()

-spec iolist() -> parser(iolist()).

Validate that input is an iolist() (a possibly-improper list of bytes, binaries, and nested iolists). Use iodata/0 if a raw binary should also be accepted.

issues(Errors)

-spec issues(errors()) -> issues().

Flatten nested errors/0 into a flat list of issue/0 records, each with a path to the failing position and a code.

Compound errors carry extra fields:

  • unknown_keys issues include keys => [term()].
  • no_match issues include branches => [issues()], one per union branch in input order.
  • invalid_key issues (from map_of/2 key validation) include key => term() (the offending key) and errors => issues().

lazy(Thunk)

-spec lazy(fun(() -> parser(T))) -> parser(T).

Defer construction of a parser until parse time. Use to build self-referential (recursive) schemas without infinite recursion at definition time.

Thunk is called on every descent into the lazy parser, so it should be cheap (typically just fun() -> some_parser_fn() end). The thunk must return a fresh parser — returning the lazy parser itself would loop forever at parse time.

tree() ->
    zz:union([
        zz:literal(leaf),
        zz:tuple({
            zz:literal(node),
            zz:lazy(fun() -> tree() end),
            zz:lazy(fun() -> tree() end)
        })
    ]).

list()

-spec list() -> parser([term()]).

Validate that input is a list (any contents).

list(Z)

-spec list(parser(T)) -> parser([T]).

Equivalent to list/2.

list(Z, Options)

-spec list(parser(T), list_options()) -> parser([T]).

Validate a homogeneous list, parsing each element with Z. Optional min/max constrain length.

literal(T)

-spec literal(T) -> parser(T).

Validate that input equals Value exactly (=:=).

map()

-spec map() -> parser(#{term() => term()}).

Validate that input is a map (passthrough on contents).

map(Schema)

-spec map(schema()) -> parser(#{term() => term()}).

Equivalent to map/2.

map(Schema, Options)

-spec map(schema(), map_options()) -> parser(#{term() => term()}).

Validate a map against Schema. unknown_keys controls handling of keys not in Schema: strip (drop, default), passthrough (keep), strict (error).

map_of(KZ, VZ)

-spec map_of(parser(K), parser(V)) -> parser(#{K => V}).

Validate a homogeneous map where every key is parsed by KZ and every value by VZ. Use this for caches, dictionaries, and other arbitrary- keyed maps where the key shape is uniform.

Key errors are wrapped as {map_key, OriginalKey, InnerErrors}; value errors are wrapped as {map_value, OriginalKey, InnerErrors}.

zz:map_of(zz:binary(), zz:integer()).

neg_integer()

-spec neg_integer() -> parser(neg_integer()).

Validate that input is a negative integer (=< -1).

non_neg_integer()

-spec non_neg_integer() -> parser(non_neg_integer()).

Validate that input is a non-negative integer (>= 0).

nullable(Z)

-spec nullable(parser(T)) -> parser(T | undefined).

Validate that input is undefined or matches Z. Sugar for union([literal(undefined), Z]).

number()

-spec number() -> parser(number()).

Validate that input is a number (integer or float).

optional(Z)

-spec optional(parser(T)) -> optional_parser(T).

Mark a parser as optional in a schema/0. Inside a map/2 schema, an optional key may be absent without producing an error.

parse(Z, Input)

-spec parse(parser(T), term()) -> result(T).

Run parser Z against Input.

pid()

-spec pid() -> parser(pid()).

Validate that input is a process identifier.

pos_integer()

-spec pos_integer() -> parser(pos_integer()).

Validate that input is a positive integer (>= 1).

reference()

-spec reference() -> parser(reference()).

Validate that input is a reference (e.g. from make_ref/0).

tuple()

-spec tuple() -> parser(tuple()).

Validate that input is a tuple (passthrough on contents).

tuple(Zs)

-spec tuple(tuple()) -> parser(tuple()).

Validate a fixed-arity tuple where each element is parsed by the corresponding parser at the same position in Zs. Element errors are wrapped as {tuple, Index, InnerErrors} with 1-based Index.

union(Zs)

-spec union([parser(T)]) -> parser(T).

Validate against the first parser that succeeds. If none match, returns {error, [{no_match, [Errors1, Errors2, ...]}]} where each entry is the errors list from the corresponding parser, in input order. Empty union yields {error, [{no_match, []}]}.