A tiny, zero-dependency parsing library for external payloads.

Mold parses JSON APIs, webhooks, HTTP params and other external input into clean Elixir terms.

See the Cheatsheet for a quick reference.

Types

A Mold type is plain Elixir data. Every type is one of three things:

FormExampleMeaning
Atom:stringBuilt-in type with default options
Function&MyApp.parse_email/1Custom parse fn value -> {:ok, v} | {:error, r} | :error end
Tuple{:integer, min: 0}Type (atom or function) with options

Options refine the type: :integer is one type, {:integer, min: 0} is a different, more precise type — like saying positive_integer. Each combination describes a specific set of values, not a type with separate validation rules bolted on.

Maps and lists have a shortcut syntax:

ShortcutExampleExpands to
Map%{name: :string}{:map, fields: [name: :string]}
List[:string]{:list, type: :string}

Shortcuts support options via tuple: {%{name: :string}, nilable: true}, {[:string], reject_invalid: true}. These can be nested: [%{name: :string}] is a list of maps.

Shortcuts don't support field options like source: or optional:. Use the full {:map, fields: [...]} syntax when you need those.

See the type definitions below for details and examples on each built-in type.

Shared options

All types accept the following options:

  • :nilable – allows nil as a valid value.
  • :default – substitutes value when nil. Accepts a static value, a zero-arity function, or an MFA tuple {mod, fun, args} for lazy evaluation. Implies nilable. Note: a 3-tuple {atom, atom, list} is always treated as MFA. To use such a tuple as a static default, wrap it: default: fn -> {Mod, :fun, []} end.
  • :in – validates that the parsed value is a member of the given Enumerable.t/0 (list, range, MapSet, etc.).
  • :transform – a function applied to the parsed value before validation. E.g. {:string, transform: &String.downcase/1}.
  • :validate – a function that must return true for the value to be accepted. Runs after :transform and :in. Fails with reason :validation_failed on false. E.g. {:integer, validate: &(rem(&1, 2) == 0)}.

Execution order: parse → transform → in → validate.

Errors include a :trace that points to the exact failing path (list indexes and/or map keys).

Summary

Types: Basic

Atom type. Accepts atoms and strings convertible to existing atoms (via String.to_existing_atom/1).

Boolean type. Accepts booleans, strings ("true", "false", "1", "0"), and integers (1, 0).

Float type. Accepts floats, integers (promoted to float), and strings parseable as floats.

Integer type. Accepts integers and strings parseable as integers.

String type. Accepts binaries; trims whitespace by default.

Types: Date & Time

Date type. Accepts Date.t/0 or ISO8601 date string. Empty strings are treated as nil.

DateTime type. Accepts DateTime.t/0 or ISO8601 datetime string. Empty strings are treated as nil.

NaiveDateTime type. Accepts NaiveDateTime.t/0 or ISO8601 datetime string (without timezone). Empty strings are treated as nil.

Time type. Accepts Time.t/0 or ISO8601 time string. Empty strings are treated as nil.

Types: Collections

List type. Validates each element against the given type.

Map type. Three forms

Tuple type. Validates each element positionally. Accepts both tuples and lists as input.

Types: Composite

Union type. Selects which type to apply based on the value.

Types: Custom

Function type. A bare parse_function/0 or a tuple with shared options.

A function that attempts to parse a value.

Types

Default value: a static value, a zero-arity function, or an MFA tuple.

t()

Union of all built-in and custom types accepted by parse/2 and parse!/2.

Transform function applied to the parsed value.

Validate function. Must return true for the value to be accepted.

Functions

Parses data according to type and returns {:ok, value} or {:error, [%Mold.Error{}]}.

Parses data according to type or raises Mold.Error.t/0 on failure.

Types: Basic

Primitive scalar types: strings, integers, floats, booleans, and atoms.

atom_type()

@type atom_type() ::
  {:atom,
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :atom

Atom type. Accepts atoms and strings convertible to existing atoms (via String.to_existing_atom/1).

Options:

Examples

iex> Mold.parse({:atom, in: [:draft, :published, :archived]}, "draft")
{:ok, :draft}

iex> Mold.parse(:atom, "nonexistent_atom_xxx")
{:error, [%Mold.Error{reason: :unknown_atom, value: "nonexistent_atom_xxx"}]}

iex> Mold.parse({:atom, in: [:draft, :published]}, "archived")
{:error, [%Mold.Error{reason: {:not_in, [:draft, :published]}, value: :archived}]}

iex> Mold.parse({:atom, default: :draft}, nil)
{:ok, :draft}

boolean_type()

@type boolean_type() ::
  {:boolean,
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :boolean

Boolean type. Accepts booleans, strings ("true", "false", "1", "0"), and integers (1, 0).

Options:

Examples

iex> Mold.parse(:boolean, "true")
{:ok, true}

iex> Mold.parse(:boolean, "0")
{:ok, false}

iex> Mold.parse(:boolean, "yes")
{:error, [%Mold.Error{reason: :invalid_format, value: "yes"}]}

iex> Mold.parse({:boolean, default: false}, nil)
{:ok, false}

float_type()

@type float_type() ::
  {:float,
   nilable: boolean(),
   default: default(),
   min: number(),
   max: number(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :float

Float type. Accepts floats, integers (promoted to float), and strings parseable as floats.

Options:

  • :min – minimum value (inclusive)
  • :max – maximum value (inclusive)
  • Shared options

Examples

iex> Mold.parse(:float, "3.14")
{:ok, 3.14}

iex> Mold.parse(:float, 42)
{:ok, 42.0}

iex> Mold.parse({:float, min: 0.0, max: 1.0}, "1.5")
{:error, [%Mold.Error{reason: {:too_large, max: 1.0}, value: 1.5}]}

integer_type()

@type integer_type() ::
  {:integer,
   nilable: boolean(),
   default: default(),
   min: integer(),
   max: integer(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :integer

Integer type. Accepts integers and strings parseable as integers.

Options:

  • :min – minimum value (inclusive)
  • :max – maximum value (inclusive)
  • Shared options

Examples

iex> Mold.parse(:integer, "42")
{:ok, 42}

iex> Mold.parse({:integer, min: 0, max: 100}, "150")
{:error, [%Mold.Error{reason: {:too_large, max: 100}, value: 150}]}

iex> Mold.parse(:integer, "abc")
{:error, [%Mold.Error{reason: :invalid_format, value: "abc"}]}

iex> Mold.parse({:integer, in: 1..10}, "5")
{:ok, 5}

string_type()

@type string_type() ::
  {:string,
   trim: boolean(),
   nilable: boolean(),
   default: default(),
   format: Regex.t(),
   min_length: non_neg_integer(),
   max_length: non_neg_integer(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :string

String type. Accepts binaries; trims whitespace by default.

Empty strings (and whitespace-only with trim) are treated as nil.

Options:

  • :trim – trim whitespace before validation (default: true)
  • :format – validate against a Regex.t/0 pattern
  • :min_length – minimum string length (inclusive, in grapheme clusters)
  • :max_length – maximum string length (inclusive, in grapheme clusters)
  • Shared options

Examples

iex> Mold.parse(:string, "  hello  ")
{:ok, "hello"}

iex> Mold.parse(:string, "   ")
{:error, [%Mold.Error{reason: :unexpected_nil, value: "   "}]}

iex> Mold.parse({:string, nilable: true}, "")
{:ok, nil}

iex> Mold.parse({:string, min_length: 3, max_length: 50}, "hi")
{:error, [%Mold.Error{reason: {:too_short, min_length: 3}, value: "hi"}]}

iex> format = ~r/^[a-z]+$/
iex> Mold.parse({:string, format: format}, "Hello")
{:error, [%Mold.Error{reason: {:invalid_format, format}, value: "Hello"}]}

iex> Mold.parse({:string, trim: false}, "  hello  ")
{:ok, "  hello  "}

Types: Date & Time

ISO8601-based date and time types.

date_type()

@type date_type() ::
  {:date,
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :date

Date type. Accepts Date.t/0 or ISO8601 date string. Empty strings are treated as nil.

Options:

Examples

iex> Mold.parse(:date, "2024-01-02")
{:ok, ~D[2024-01-02]}

iex> Mold.parse(:date, "2024-13-01")
{:error, [%Mold.Error{reason: :invalid_date, value: "2024-13-01"}]}

iex> year_2024 = Date.range(~D[2024-01-01], ~D[2024-12-31])
iex> Mold.parse({:date, in: year_2024}, "2025-01-01")
{:error, [%Mold.Error{reason: {:not_in, year_2024}, value: ~D[2025-01-01]}]}

datetime_type()

@type datetime_type() ::
  {:datetime,
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :datetime

DateTime type. Accepts DateTime.t/0 or ISO8601 datetime string. Empty strings are treated as nil.

Options:

Examples

iex> Mold.parse(:datetime, "2024-01-02T03:04:05Z")
{:ok, ~U[2024-01-02 03:04:05Z]}

iex> Mold.parse(:datetime, ~U[2024-01-02 03:04:05Z])
{:ok, ~U[2024-01-02 03:04:05Z]}

iex> Mold.parse(:datetime, "invalid")
{:error, [%Mold.Error{reason: :invalid_format, value: "invalid"}]}

iex> Mold.parse({:datetime, nilable: true}, "")
{:ok, nil}

naive_datetime_type()

@type naive_datetime_type() ::
  {:naive_datetime,
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :naive_datetime

NaiveDateTime type. Accepts NaiveDateTime.t/0 or ISO8601 datetime string (without timezone). Empty strings are treated as nil.

Options:

Examples

iex> Mold.parse(:naive_datetime, "2024-01-02T03:04:05")
{:ok, ~N[2024-01-02 03:04:05]}

iex> Mold.parse(:naive_datetime, "invalid")
{:error, [%Mold.Error{reason: :invalid_format, value: "invalid"}]}

time_type()

@type time_type() ::
  {:time,
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :time

Time type. Accepts Time.t/0 or ISO8601 time string. Empty strings are treated as nil.

Options:

Examples

iex> Mold.parse(:time, "14:30:00")
{:ok, ~T[14:30:00]}

iex> Mold.parse(:time, "25:00:00")
{:error, [%Mold.Error{reason: :invalid_time, value: "25:00:00"}]}

Types: Collections

Maps, lists, and tuples.

list_type()

@type list_type() ::
  {:list,
   type: t(),
   reject_invalid: boolean(),
   min_length: non_neg_integer(),
   max_length: non_neg_integer(),
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :list

List type. Validates each element against the given type.

Bare :list validates the value is a list and returns as-is (passthrough).

The shortcut [type] can always be used instead of {:list, type: type}, including with options: {[type], opts}.

Options:

  • :type – the element type t/0 (required)
  • :reject_invalid – drop invalid items instead of failing
  • :min_length – minimum list length (inclusive)
  • :max_length – maximum list length (inclusive)
  • Shared options

Examples

iex> Mold.parse([:string], ["a", "b"])
{:ok, ["a", "b"]}

iex> Mold.parse([%{name: :string}], [%{"name" => "A"}, %{"name" => "B"}])
{:ok, [%{name: "A"}, %{name: "B"}]}

iex> Mold.parse({[:integer], min_length: 1, max_length: 3}, [])
{:error, [%Mold.Error{reason: {:too_short, min_length: 1}, value: []}]}

iex> Mold.parse({[:string], reject_invalid: true}, ["a", nil, "b"])
{:ok, ["a", "b"]}

iex> Mold.parse([:integer], ["1", "abc", "3"])
{:error, [%Mold.Error{reason: :invalid_format, value: "abc", trace: [1]}]}

map_type()

@type map_type() ::
  {:map,
   fields: [
     {term(), t() | [type: t(), source: source_path(), optional: boolean()]}
   ],
   keys: t(),
   values: t(),
   source: (term() -> any()),
   reject_invalid: boolean(),
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :map

Map type. Three forms:

  • Bare :map — validates the value is a map, returns as-is (passthrough).
  • {:map, fields: [...]} — validates maps field-by-field. Field names are typically atoms (the default source converts them to strings for lookup); non-atom keys require a custom source.
  • {:map, keys: t, values: t} — homogeneous map, parses all keys and values.

Options for fields:

  • :fields – list of {name, type} tuples where each value is either a t/0 (name: :string) or a keyword list with field options:
    • :type – the field type t/0 (required)
    • :source – where to read the value from. A single step or a list of steps (path), like get_in/2. Each step can be:
    • :optional – when true, omit field from result when missing from input
  • :source – a function (field_name -> any()) that derives the source key from each field name (e.g. source: &(Atom.to_string(&1) |> Macro.camelize())). Defaults to &Atom.to_string/1. Propagates recursively to nested maps, including through lists and tuples.

Options for keys/values:

  • :keys – type t/0 for all keys
  • :values – type t/0 for all values

Both fields and keys/values forms support:

  • :reject_invalid – drop items that fail parsing instead of returning an error. For fields, only affects fields marked optional: true — required fields still fail. For keys/values, drops the key-value pair that failed.

Shared options apply to all forms.

Examples

iex> Mold.parse(%{name: :string, age: :integer}, %{"name" => "Alice", "age" => "25"})
{:ok, %{name: "Alice", age: 25}}

Source defaults to &Atom.to_string/1 — field names are looked up as string keys. Custom source per field:

iex> schema = {:map, fields: [
...>   user_name: [type: :string, source: "userName"],
...>   is_active: [type: :boolean, source: "isActive"]
...> ]}
iex> Mold.parse(schema, %{"userName" => "Alice", "isActive" => "true"})
{:ok, %{user_name: "Alice", is_active: true}}

Nested source paths:

iex> schema = {:map, fields: [name: [type: :string, source: ["details", "name"]]]}
iex> Mold.parse(schema, %{"details" => %{"name" => "Alice"}})
{:ok, %{name: "Alice"}}

Access functions for advanced navigation — Access.at/1 for lists, Access.elem/1 for tuples, Access.key/2 with a default, and more:

iex> schema = {:map, fields: [
...>   lat: [type: :float, source: ["coords", Access.at(0)]],
...>   lng: [type: :float, source: ["coords", Access.at(1)]]
...> ]}
iex> Mold.parse(schema, %{"coords" => [49.8, 24.0]})
{:ok, %{lat: 49.8, lng: 24.0}}

Global source function (propagates to nested maps):

iex> schema = {
...>   %{user_name: :string, address: %{zip_code: :string}},
...>   source: &(Atom.to_string(&1) |> Macro.camelize())
...> }
iex> Mold.parse(schema, %{"UserName" => "Alice", "Address" => %{"ZipCode" => "10001"}})
{:ok, %{user_name: "Alice", address: %{zip_code: "10001"}}}

Non-atom field names work with a custom source:

iex> schema = {:map,
...>   source: fn {ns, name} -> Enum.join([ns, name], ":") end,
...>   fields: [
...>     {{:feature, :dark_mode}, :boolean},
...>     {{:feature, :beta}, :boolean}
...>   ]}
iex> Mold.parse(schema, %{"feature:dark_mode" => "true", "feature:beta" => "0"})
{:ok, %{{:feature, :dark_mode} => true, {:feature, :beta} => false}}

Optional fields:

iex> schema = {:map, fields: [
...>   name: :string,
...>   bio: [type: :string, optional: true]
...> ]}
iex> Mold.parse(schema, %{"name" => "Alice"})
{:ok, %{name: "Alice"}}
iex> Mold.parse(schema, %{"name" => "Alice", "bio" => "hello"})
{:ok, %{name: "Alice", bio: "hello"}}
iex> # optional omits the field when missing, but nil still fails — use nilable on the type
iex> Mold.parse(schema, %{"name" => "Alice", "bio" => nil})
{:error, [%Mold.Error{reason: :unexpected_nil, value: nil, trace: ["bio"]}]}

When a field is both optional: true and has a default, optional takes priority for missing fields — the field is omitted from the result. The default only applies when the field is present but nil:

iex> schema = {:map, fields: [
...>   bio: [type: {:string, default: "none"}, optional: true]
...> ]}
iex> Mold.parse(schema, %{})
{:ok, %{}}
iex> Mold.parse(schema, %{"bio" => nil})
{:ok, %{bio: "none"}}

Missing fields are errors:

iex> Mold.parse(%{name: :string}, %{})
{:error, [%Mold.Error{reason: {:missing_field, "name"}, value: %{}, trace: []}]}

Homogeneous maps with keys/values:

iex> Mold.parse({:map, keys: :string, values: :integer}, %{"a" => "1", "b" => "2"})
{:ok, %{"a" => 1, "b" => 2}}

iex> Mold.parse({:map, keys: :atom, values: :string}, %{"name" => "Alice"})
{:ok, %{name: "Alice"}}

Reject invalid — for fields, only optional fields are affected:

iex> schema = {:map, reject_invalid: true, fields: [
...>   name: :string,
...>   age: [type: :integer, optional: true],
...>   bio: [type: :string, optional: true]
...> ]}
iex> Mold.parse(schema, %{"name" => "Alice", "age" => "nope", "bio" => nil})
{:ok, %{name: "Alice"}}
iex> Mold.parse(schema, %{"age" => "25"})
{:error, [%Mold.Error{reason: {:missing_field, "name"}, value: %{"age" => "25"}, trace: []}]}

Reject invalid — for keys/values, drops the key-value pair that failed:

iex> Mold.parse({:map, keys: :string, values: :integer, reject_invalid: true}, %{"a" => "1", "b" => "nope"})
{:ok, %{"a" => 1}}

tuple_type()

@type tuple_type() ::
  {:tuple,
   elements: [t()],
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | :tuple

Tuple type. Validates each element positionally. Accepts both tuples and lists as input.

Bare :tuple validates the value is a tuple (or list), converts lists to tuples, returns as-is.

Options:

Examples

iex> Mold.parse({:tuple, elements: [:string, :integer]}, ["Alice", "25"])
{:ok, {"Alice", 25}}

iex> Mold.parse({:tuple, elements: [:string, :integer]}, {"Alice", "25"})
{:ok, {"Alice", 25}}

iex> Mold.parse({:tuple, elements: [:string, :integer]}, ["only_one"])
{:error, [%Mold.Error{reason: {:unexpected_length, expected: 2, got: 1}, value: ["only_one"]}]}

iex> Mold.parse(:tuple, [1, "two", :three])
{:ok, {1, "two", :three}}

Types: Composite

Types that combine other types.

union_type()

@type union_type() ::
  {:union,
   by: (any() -> any()),
   of: %{required(any()) => t()},
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}

Union type. Selects which type to apply based on the value.

The by function receives the raw value and must return a variant that is looked up in of.

Options:

  • :by – function that takes the raw value and returns a variant (required)
  • :of – map of variant => t/0 (required)
  • Shared options

Examples

iex> schema = {:union,
...>   by: fn value -> value["type"] end,
...>   of: %{
...>     "user" => %{name: :string},
...>     "bot"  => %{version: :integer}
...>   }}
iex> Mold.parse(schema, %{"type" => "user", "name" => "Alice"})
{:ok, %{name: "Alice"}}
iex> Mold.parse(schema, %{"type" => "bot", "version" => "3"})
{:ok, %{version: 3}}
iex> Mold.parse(schema, %{"type" => "admin"})
{:error, [%Mold.Error{reason: {:unknown_variant, "admin"}, value: %{"type" => "admin"}}]}

Use a catch-all in by to handle unexpected input types:

iex> schema = {:union,
...>   by: fn
...>     %{"type" => type} -> type
...>     _ -> :unknown
...>   end,
...>   of: %{
...>     "user" => %{name: :string},
...>     "bot"  => %{version: :integer}
...>   }}
iex> Mold.parse(schema, 123)
{:error, [%Mold.Error{reason: {:unknown_variant, :unknown}, value: 123}]}

Types: Custom

Any fn value -> {:ok, parsed} | {:error, reason} | :error end is accepted wherever a type is expected. Wrap in a tuple to add shared options: {my_fn, nilable: true}.

function_type()

@type function_type() ::
  {parse_function(),
   nilable: boolean(),
   default: default(),
   in: Enumerable.t(),
   transform: transform(),
   validate: validate()}
  | parse_function()

Function type. A bare parse_function/0 or a tuple with shared options.

Examples

iex> email_type = fn v ->
...>   if is_binary(v) and String.contains?(v, "@") do
...>     {:ok, String.downcase(v)}
...>   else
...>     {:error, :invalid_email}
...>   end
...> end
iex> Mold.parse(email_type, "USER@EXAMPLE.COM")
{:ok, "user@example.com"}
iex> Mold.parse({email_type, nilable: true}, nil)
{:ok, nil}
iex> Mold.parse({email_type, default: "fallback@example.com"}, nil)
{:ok, "fallback@example.com"}
iex> # Use inside maps
iex> Mold.parse(%{email: email_type}, %{"email" => "USER@EXAMPLE.COM"})
{:ok, %{email: "user@example.com"}}
iex> # Standard library functions work as types
iex> Mold.parse(&JSON.decode/1, ~s|{"a": 1}|)
{:ok, %{"a" => 1}}

Bare :error is supported (see parse_function/0):

iex> Mold.parse(&Version.parse/1, "1.0.0")
{:ok, %Version{major: 1, minor: 0, patch: 0}}
iex> Mold.parse(&Version.parse/1, "invalid")
{:error, [%Mold.Error{reason: :invalid, value: "invalid"}]}

parse_function()

@type parse_function() :: (any() ->
                       {:ok, any()}
                       | {:error, [Mold.Error.t()] | any()}
                       | :error)

A function that attempts to parse a value.

Must return {:ok, parsed}, {:error, reason}, or bare :error.

Bare :error is converted to {:error, :invalid} — this lets you use standard library functions like &Version.parse/1 directly as types.

Can also return {:error, [%Mold.Error{}]} — errors are passed through with trace propagation. This is useful when a parse function calls Mold.parse/2 internally, including recursive types:

defmodule Comment do
  def parse_comment(value) do
    Mold.parse(%{text: :string, replies: {[&parse_comment/1], nilable: true}}, value)
  end
end

Types

default()

@type default() :: any() | (-> any()) | {module(), atom(), [any()]}

Default value: a static value, a zero-arity function, or an MFA tuple.

source_path()

@type source_path() :: source_step() | [source_step()]

source_step()

@type source_step() :: term() | Access.access_fun(any(), any())

t()

@type t() ::
  string_type()
  | atom_type()
  | boolean_type()
  | integer_type()
  | float_type()
  | datetime_type()
  | naive_datetime_type()
  | date_type()
  | time_type()
  | union_type()
  | map_type()
  | list_type()
  | tuple_type()
  | function_type()
  | %{optional(any()) => t()}
  | {%{optional(any()) => t()}, keyword()}
  | [t()]
  | {[t()], keyword()}

Union of all built-in and custom types accepted by parse/2 and parse!/2.

transform()

@type transform() :: (any() -> any())

Transform function applied to the parsed value.

validate()

@type validate() :: (any() -> boolean())

Validate function. Must return true for the value to be accepted.

Functions

parse(type, data)

@spec parse(t(), data :: any()) ::
  {:ok, result :: any()} | {:error, [Mold.Error.t(), ...]}

Parses data according to type and returns {:ok, value} or {:error, [%Mold.Error{}]}.

See t/0 for all accepted type forms.

iex> Mold.parse({:string, nilable: true, trim: true}, "  ")
{:ok, nil}

iex> Mold.parse(:boolean, "false")
{:ok, false}

parse!(type, data)

@spec parse!(t(), any()) :: any() | no_return()

Parses data according to type or raises Mold.Error.t/0 on failure.

iex> Mold.parse!(%{name: :string}, %{"name" => "Bob"})
%{name: "Bob"}

Multiple errors are combined into a single exception:

Mold.parse!(%{name: :string, age: :integer}, %{"name" => nil, "age" => "abc"})
#=> ** (Mold.Error) Unable to parse data
#=>
#=> 2 errors:
#=>   1. :unexpected_nil at ["name"] (value: nil)
#=>   2. :invalid_format at ["age"] (value: "abc")