Predicator supports nested data structure access using both dot notation and bracket notation, letting you reference deeply nested values in your context.
The examples below use the card-authorization context the rest of the docs
authorize against: an account with a holder, a budget and a status, the
transaction being decided, and two lists - the signup wizard's steps and
a run of recent amounts.
iex> context = %{"account" => %{"holder" => %{"first" => "Ada", "last" => "Byron"}, "budget" => %{"remaining" => 500, "limit" => 2000}, "status" => %{"card_active" => true, "tier" => "gold"}}, "transaction" => %{"amount" => 120, "currency" => "USD"}, "steps" => ["payment", "review", "confirm"], "amounts" => [85, 92, 78, 96]}
iex> Predicator.evaluate("account.holder.first == 'Ada'", context)
{:ok, true}
iex> context = %{"account" => %{"holder" => %{"first" => "Ada", "last" => "Byron"}, "budget" => %{"remaining" => 500, "limit" => 2000}, "status" => %{"card_active" => true, "tier" => "gold"}}, "transaction" => %{"amount" => 120, "currency" => "USD"}, "steps" => ["payment", "review", "confirm"], "amounts" => [85, 92, 78, 96]}
iex> Predicator.evaluate("transaction.amount <= account.budget.remaining", context)
{:ok, true}
iex> context = %{"account" => %{"holder" => %{"first" => "Ada", "last" => "Byron"}, "budget" => %{"remaining" => 500, "limit" => 2000}, "status" => %{"card_active" => true, "tier" => "gold"}}, "transaction" => %{"amount" => 120, "currency" => "USD"}, "steps" => ["payment", "review", "confirm"], "amounts" => [85, 92, 78, 96]}
iex> Predicator.evaluate("account.budget.limit == 2000", context)
{:ok, true}Bracket notation
iex> context = %{"account" => %{"holder" => %{"first" => "Ada"}, "status" => %{"tier" => "gold"}, "budget" => %{"remaining" => 500}}, "steps" => ["payment"], "amounts" => [85, 92]}
iex> Predicator.evaluate("account['holder']['first'] == 'Ada'", context)
{:ok, true}
iex> context = %{"account" => %{"holder" => %{"first" => "Ada"}, "status" => %{"tier" => "gold"}, "budget" => %{"remaining" => 500}}, "steps" => ["payment"], "amounts" => [85, 92]}
iex> Predicator.evaluate("account['status']['tier'] == 'gold'", context)
{:ok, true}Array access
iex> context = %{"steps" => ["payment", "review", "confirm"], "amounts" => [85, 92, 78, 96]}
iex> Predicator.evaluate("steps[0] == 'payment'", context)
{:ok, true}
iex> context = %{"steps" => ["payment", "review", "confirm"], "amounts" => [85, 92, 78, 96]}
iex> Predicator.evaluate("amounts[1] > 90", context)
{:ok, true}
iex> context = %{"amounts" => [85, 92, 78, 96], "index" => 2}
iex> Predicator.evaluate("amounts[index] > 80", context)
{:ok, false}
iex> context = %{"steps" => ["payment", "review", "confirm"], "i" => 1}
iex> Predicator.evaluate("steps[i + 1]", context)
{:ok, "confirm"}
iex> context = %{"steps" => ["payment", "review"]}
iex> Predicator.evaluate("steps[5] == 'confirm'", context)
{:ok, :undefined}Bracket keys accept any expression, not just a bare variable - steps[i + 1]
computes the index before indexing. An out-of-bounds index returns
:undefined rather than raising, the same as a missing map key.
Mixed notation
iex> context = %{"account" => %{"status" => %{"tier" => "gold"}, "budget" => %{"remaining" => 500}}}
iex> Predicator.evaluate("account.status['tier'] == 'gold'", context)
{:ok, true}
iex> context = %{"account" => %{"status" => %{"tier" => "gold"}, "budget" => %{"remaining" => 500}}}
iex> Predicator.evaluate("account['budget'].remaining == 500", context)
{:ok, true}Chained access and combined expressions
iex> context = %{"account" => %{"holder" => %{"first" => "Ada", "last" => "Byron"}, "budget" => %{"remaining" => 500}, "status" => %{"card_active" => true}}}
iex> Predicator.evaluate("account['holder']['first'] + ' ' + account['holder']['last']", context)
{:ok, "Ada Byron"}
iex> context = %{"account" => %{"budget" => %{"remaining" => 500}, "status" => %{"card_active" => true}}}
iex> Predicator.evaluate("account.budget.remaining >= 100 AND account.status.card_active", context)
{:ok, true}Missing paths
A missing path returns :undefined rather than raising:
iex> context = %{"account" => %{"holder" => %{"first" => "Ada"}}}
iex> Predicator.evaluate("account.holder.email == 'ada@example.com'", context)
{:ok, :undefined}Atom keys in the context you pass in
A context built with atom keys still works, but not because nested access
reads atom keys directly - Predicator.evaluate/3 normalizes a bare map
through Predicator.Context.new/2 before evaluation, which converts atom
keys to string keys deeply and eagerly. By the time access runs, only string
keys remain:
iex> atom_context = %{account: %{holder: %{first: "Ada"}}}
iex> Predicator.evaluate("account.holder.first == 'Ada'", atom_context)
{:ok, true}Nested lists
iex> list_context = %{"account" => %{"networks" => ["visa", "mastercard"]}}
iex> Predicator.evaluate("'visa' in account.networks", list_context)
{:ok, true}Summary
- Dot notation:
account.holder.firstfor nested object access - Bracket notation:
account['holder']['first']for dynamic key access - Array indexing:
steps[0],amounts[index]for list access - Mixed styles:
account.status['tier']combining both notations - Unlimited nesting depth:
account.budget.limits.daily.ceiling - Dynamic and expression keys:
steps[index],steps[i + 1] - Graceful fallback: returns
:undefinedfor missing paths or out-of-bounds access - Type preservation: maintains original data types (strings, numbers, booleans, lists)
- Backwards compatible: simple variable names work exactly as before