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
| Operator | Symbol | Semantics |
|---|---|---|
Equals / NotEquals | == / != | Value equality |
LessThan / LessThanOrEqual / GreaterThan / GreaterThanOrEqual | < / <= / > / >= | Numeric comparison |
In / NotIn | in / not in | The actual value appears (or does not) in the expected array |
Contains | contains | The actual array contains the expected value |
Matches | matches | Regular-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_infor closed vocabularies and explicit comparisons for numbers; each reads directly in the rule and maps to an operator an auditor can check. - Use
$.field[*]withis_into restrict every element of an array field such aslocations. - 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-matchersonly 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).