Primitives

String

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

Mold.parse({:string, min_length: 3, max_length: 50}, "hello")
#=> {:ok, "hello"}

Mold.parse({:string, format: ~r/^[a-z]+$/}, "abc")
#=> {:ok, "abc"}

# empty strings → nil
Mold.parse({:string, nilable: true}, "")
#=> {:ok, nil}

Integer

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

Mold.parse({:integer, min: 0, max: 100}, "50")
#=> {:ok, 50}

Float

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

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

Boolean

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

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

Atom

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

Mold.parse(:atom, :existing_atom)
#=> {:ok, :existing_atom}

Date & Time

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

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

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

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

Maps

Basic

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

Source mapping

# Global source function (propagates to nested maps, lists, and tuples)
schema = {%{
  user_name: :string,
  address: %{zip_code: :string}
}, source: &(Atom.to_string(&1) |> Macro.camelize())}

Mold.parse(schema, %{"UserName" => "Alice", "Address" => %{"ZipCode" => "10001"}})
#=> {:ok, %{user_name: "Alice", address: %{zip_code: "10001"}}}
# Per-field source
schema = {:map, fields: [
  user_name: [type: :string, source: "userName"],
  is_active: [type: :boolean, source: "isActive"]
]}

# Nested source path
schema = {:map, fields: [
  email: [type: :string, source: ["sender", "emailAddress", "address"]]
]}

# Access functions in source path (like get_in/2)
schema = {:map, fields: [
  lat: [type: :float, source: ["coords", Access.at(0)]],
  lng: [type: :float, source: ["coords", Access.at(1)]]
]}

Mold.parse(schema, %{"coords" => [49.8, 24.0]})
#=> {:ok, %{lat: 49.8, lng: 24.0}}

# Non-atom field names with custom source
schema = {:map,
  source: fn {ns, name} -> "#{ns}:#{name}" end,
  fields: [
    {{:feature, :dark_mode}, :boolean},
    {{:feature, :beta}, :boolean}
  ]}

Mold.parse(schema, %{"feature:dark_mode" => "true", "feature:beta" => "0"})
#=> {:ok, %{{:feature, :dark_mode} => true, {:feature, :beta} => false}}

Homogeneous maps

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

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

Optional fields

schema = {:map, fields: [
  name: :string,
  bio: [type: :string, optional: true]
]}

Mold.parse(schema, %{"name" => "Alice"})
#=> {:ok, %{name: "Alice"}}

Reject invalid

# Fields — only optional fields are dropped on error
schema = {:map, reject_invalid: true, fields: [
  name: :string,
  age: [type: :integer, optional: true]
]}

Mold.parse(schema, %{"name" => "Alice", "age" => "nope"})
#=> {:ok, %{name: "Alice"}}

# Homogeneous maps — drops the key-value pair that failed
Mold.parse({:map, keys: :string, values: :integer,
  reject_invalid: true}, %{"a" => "1", "b" => "nope"})
#=> {:ok, %{"a" => 1}}

Lists & Tuples

Lists

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

Mold.parse({:list, type: :integer,
  min_length: 1, max_length: 10}, [1, 2, 3])
#=> {:ok, [1, 2, 3]}

# Drop invalid items
Mold.parse({[:string], reject_invalid: true},
  ["a", nil, "b"])
#=> {:ok, ["a", "b"]}

Tuples

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

# Also accepts tuples as input
Mold.parse({:tuple, elements: [:string, :integer]},
  {"Alice", "25"})
#=> {:ok, {"Alice", 25}}

Advanced

Union types

schema = {:union,
  by: fn value -> value["type"] end,
  of: %{
    "user" => %{name: :string},
    "bot"  => %{version: :integer}
  }}

Mold.parse(schema, %{"type" => "user", "name" => "Alice"})
#=> {:ok, %{name: "Alice"}}

Recursive types

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

Custom parse functions

email_type = fn value ->
  if is_binary(value) and String.contains?(value, "@"),
    do: {:ok, String.downcase(value)},
    else: {:error, :invalid_email}
end

Mold.parse(email_type, "USER@EXAMPLE.COM")
#=> {:ok, "user@example.com"}

# Standard library functions work too
Mold.parse(&Version.parse/1, "1.0.0")
#=> {:ok, %Version{major: 1, minor: 0, patch: 0}}

Default values

Mold.parse({:integer, default: 0}, nil)
#=> {:ok, 0}

# Lazy evaluation
Mold.parse({:string, default: fn -> "gen" end}, nil)
#=> {:ok, "gen"}

# MFA tuple
Mold.parse({:integer,
  default: {Enum, :count, [[1, 2, 3]]}}, nil)
#=> {:ok, 3}

Transform & validate

Mold.parse({:string,
  transform: &String.downcase/1}, "HELLO")
#=> {:ok, "hello"}

Mold.parse({:integer,
  validate: &(&1 > 0)}, "-1")
#=> {:error, [%Mold.Error{
#=>   reason: :validation_failed, ...}]}

# Execution order: parse → transform → in → validate

Types reference

All types

TypeInputOutput
:stringbinaryString.t()
:atomatom, stringatom()
:booleanboolean, "true"/"false", "1"/"0", 1/0boolean()
:integerinteger, stringinteger()
:floatfloat, integer, stringfloat()
:dateDate.t(), ISO8601 stringDate.t()
:datetimeDateTime.t(), ISO8601 stringDateTime.t()
:naive_datetimeNaiveDateTime.t(), ISO8601 stringNaiveDateTime.t()
:timeTime.t(), ISO8601 stringTime.t()
:mapmapmap (passthrough)
{:map, fields: [...]}mapmap with field name keys
{:map, keys: t, values: t}mapmap with parsed keys and values
:listlistlist (passthrough)
{:list, type: t}listlist of parsed values
:tupletuple, listtuple (passthrough)
{:tuple, elements: [t]}tuple, listtuple of parsed values
{:union, by: fn, of: %{}}anydepends on matched type
fn v -> {:ok, v} | {:error, r} | :error endanyany

Options

Shared (all types)

OptionDescription
nilable: trueAccept nil as valid
default: v | fn | mfaSubstitute when nil (implies nilable)
in: enumerableValidate membership
transform: fnTransform parsed value (before in and validate)
validate: fnMust return true (after transform and in)

Type-specific

OptionApplies to
min: n / max: n:integer, :float
min_length: n / max_length: n:string, :list
trim: true (default):string
format: ~r//:string
source: fn:map
source: key | [path]map fields (path steps: strings, Access fns)
optional: truemap fields
reject_invalid: true:list, :map