Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Field Matching

Structural validation only checks shape. Your server also needs to enforce policy on the contents of a detail: payments in EUR only, never more than 1000.00 per transfer, actions restricted to a known vocabulary.

authz-details-rs expresses such conditions as FieldMatcher values: a JSONPath, a comparison operator, and an expected value. Rules are built from them (see Rules and Policies); this page covers the matchers themselves.

Creating a field matcher

Each operator has a typed constructor; values are anything that converts into a serde_json::Value (numbers, strings, booleans, json! values), and list operators take any iterator:

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;

let currency = FieldMatcher::eq("$.instructedAmount.currency", "EUR");
let amount = FieldMatcher::le("$.instructedAmount.amount", 1000);
let action = FieldMatcher::contains("$.actions", "initiate");
let vocabulary = FieldMatcher::is_in("$.actions[*]", ["initiate", "status"]);

// The general form takes any ComparisonOp
let also_amount = FieldMatcher::new(
    "$.instructedAmount.amount",
    ComparisonOp::LessThanOrEqual,
    1000,
);
assert_eq!(amount, also_amount);
}

Inside a rule builder the same operators read as .field(path).le(1000).

Paths are standard JSONPath expressions rooted at the detail object, limited to 10 segments (dot and bracket segments both count) so that matching stays cheap even on hostile input. A matcher parses its path once, on first use; validate() parses it eagerly, and rule builders and policies call it for you.

Comparison operators

OperatorSymbolSemantics
Equals / NotEquals== / !=Value equality
LessThan / LessThanOrEqual / GreaterThan / GreaterThanOrEqual< / <= / > / >=Numeric comparison
In / NotInin / not inThe actual value appears (or does not) in the expected array
ContainscontainsThe actual array contains the expected value
MatchesmatchesRegular-expression match; requires the regex-matchers feature

ComparisonOp parses from its name, its snake_case name, or its symbol, which is convenient when rules come from configuration files:

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;

assert_eq!("<=".parse::<ComparisonOp>().unwrap(), ComparisonOp::LessThanOrEqual);
assert_eq!("not in".parse::<ComparisonOp>().unwrap(), ComparisonOp::NotIn);
assert_eq!("GreaterThan".parse::<ComparisonOp>().unwrap(), ComparisonOp::GreaterThan);
}

Numeric operators accept JSON numbers and numeric strings alike, since RFC 9396 encodes amounts as decimal strings ("amount": "123.50"). Comparing a number with a non-numeric value fails with ConstraintViolation::TypeMismatch; a malformed or over-deep path fails with ConstraintViolation::JsonPathError.

A JSONPath that matches nothing makes the matcher fail for every operator, including NotEquals and NotIn: omitting a field can never satisfy a condition. A path that matches several nodes (for example $.locations[*]) passes only if every matched node satisfies the operator, so a request cannot smuggle a disallowed value behind an allowed one.

Evaluating a matcher

A matcher evaluates against a JSON value, extracting the field at its path and applying the operator:

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
use serde_json::json;

let detail_object = json!({
    "actions": ["initiate"],
    "locations": ["https://example.com/payments", "https://evil.example"],
    "instructedAmount": {"currency": "EUR", "amount": "250.00"}
});

assert!(FieldMatcher::eq("$.instructedAmount.currency", "EUR").evaluate(&detail_object).unwrap());
assert!(FieldMatcher::le("$.instructedAmount.amount", 1000).evaluate(&detail_object).unwrap());
assert!(FieldMatcher::contains("$.actions", "initiate").evaluate(&detail_object).unwrap());

// Every location must be allowed, not just the first one
let locations = FieldMatcher::is_in("$.locations[*]", ["https://example.com/payments"]);
assert!(!locations.evaluate(&detail_object).unwrap());

// A missing field never satisfies a condition, not even `!=`
assert!(!FieldMatcher::ne("$.creditorName", "Shady Inn").evaluate(&detail_object).unwrap());
}

Time-based conditions (business hours, weekdays) are not field matchers: they depend on the evaluation time, not on the detail. See Temporal Rules.

Best practices

  • Prefer is_in for closed vocabularies and explicit comparisons for numbers; each reads directly in the rule and maps to an operator an auditor can check.
  • Use $.field[*] with is_in to restrict every element of an array field such as locations.
  • Keep JSONPath expressions shallow (the limit is 10 segments) and rooted at $. – deep paths are slow and usually a sign of an over-broad schema.
  • Enable regex-matchers only if you actually need pattern matching.

Next steps

Group matchers into named rules with Rules and Policies, or drive typed subset checks from a registered ComparatorConfig (see Subset Comparison).