Schema Validation
Your authorization server supports several authorization_details types:
payment_initiation for credit transfers, account_information for account
queries, and perhaps virtual-account for IBAN allocation. Each type has
its own shape, and every entry in a request must name a type you recognize
and conform to its structure.
authz-details-rs validates details against JSON Schema (Draft 2020-12) schemas registered per type.
Registering a type schema
A TypeSchema pairs a type identifier with a JSON Schema and a description:
#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
let payment_schema = TypeSchema::new(
"payment_initiation",
serde_json::json!({
"type": "object",
"required": ["actions", "instructedAmount"],
"properties": {
"actions": {
"type": "array",
"items": {
"type": "string",
"enum": ["initiate", "status", "cancel"]
}
},
"instructedAmount": {
"type": "object",
"required": ["currency", "amount"],
"properties": {
"currency": {"type": "string", "pattern": "^[A-Z]{3}$"},
"amount": {"type": "number", "minimum": 0}
}
},
"creditorAccount": {
"type": "object",
"properties": {
"iban": {"type": "string"}
}
}
}
}),
"Payment initiation authorization",
);
}
The schema constrains the detail_object only; the type field is matched
separately against the schema’s type_id.
Validating a single detail
validate_against_schema checks the type identifier, then validates the
detail object against the schema:
#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
let payment_schema = TypeSchema::new(
"payment_initiation",
serde_json::json!({
"type": "object",
"required": ["actions", "instructedAmount"],
"properties": {
"actions": {"type": "array"},
"instructedAmount": {"type": "object"}
}
}),
"Payment initiation authorization",
);
let detail = AuthorizationDetail::new(
"payment_initiation",
serde_json::json!({
"actions": ["initiate"],
"instructedAmount": {
"currency": "EUR",
"amount": 123.50
},
"creditorAccount": {
"iban": "DE02100100109307118603"
}
}),
);
assert!(validate_against_schema(&detail, &payment_schema).is_ok());
}
A detail whose type does not match the schema’s type_id is rejected
before the schema is even compiled.
Validating a request against a registry
A request carries a list of details, each possibly of a different type.
Register your types once in a TypeRegistry: it compiles each JSON Schema
when the type is registered (so a broken schema fails at startup, not on the
first request) and resolves every detail by its type:
#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
let registry = TypeRegistry::try_from(vec![
TypeSchema::new(
"payment_initiation",
serde_json::json!({
"type": "object",
"required": ["actions", "instructedAmount"]
}),
"Payment initiation authorization",
),
TypeSchema::new(
"account_information",
serde_json::json!({
"type": "object",
"required": ["actions"],
"properties": {
"actions": {"type": "array", "items": {"type": "string"}}
}
}),
"Account information",
),
])
.unwrap();
// The authorization_details request parameter, as sent on the wire
let details = AuthorizationDetails::from_json(
r#"[{"type": "account_information", "actions": ["list_accounts"]},
{"type": "payment_initiation", "actions": ["initiate"],
"instructedAmount": {"currency": "EUR", "amount": "123.50"}}]"#,
)
.unwrap();
assert!(registry.validate(&details).is_ok());
let unknown = [AuthorizationDetail::new("virtual-account", serde_json::json!({}))];
assert!(matches!(
registry.validate(&unknown),
Err(ValidationError::UnknownType { .. })
));
}
AuthorizationDetails::from_json rejects anything that is not an array of
objects with a string type (ValidationError::InvalidDetail), and
AuthorizationDetail::try_new does the same for a single object built from
untrusted input. The free function validate_details(&details, &schemas)
does the same job as registry.validate from a plain &[TypeSchema], but
compiles the schemas on every call.
Validating a raw JSON object
When you already hold the detail object as a plain serde_json::Value
(for example, straight from a request body), skip the AuthorizationDetail
wrapper and validate the object directly:
#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
let payment_schema = TypeSchema::new(
"payment_initiation",
serde_json::json!({
"type": "object",
"required": ["actions", "instructedAmount"]
}),
"Payment initiation authorization",
);
let object = serde_json::json!({
"actions": ["initiate"],
"instructedAmount": {"currency": "EUR", "amount": 10.0}
});
assert!(validate_detail_object(&object, &payment_schema.schema).is_ok());
}
Schemas must be self-contained
JSON Schema lets a schema pull in other documents with $ref.
authz-details-rs deliberately does not resolve external references: a
$ref to an http(s): URL or a file: path fails registration with
ValidationError::InvalidSchema. The resolvers are not even compiled in
(authz-details-rs builds jsonschema without its resolve-http and
resolve-file features).
This is a security measure. An authorization server that fetched whatever
a schema names could be turned into a server-side request forgery tool
against internal services, or into a way to read local files, by anyone
who can influence a schema – and a remote schema could change after it was
reviewed. Keeping schemas self-contained also keeps validation offline and
deterministic. Inline shared definitions with $defs and local $refs
instead:
#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
use serde_json::json;
// Rejected: the registry never fetches external documents
let external = TypeSchema::new(
"payment_initiation",
json!({"$ref": "https://schemas.example.com/payment_initiation.json"}),
"Payment initiation",
);
assert!(matches!(
TypeRegistry::new().with_type(external),
Err(ValidationError::InvalidSchema(_))
));
// Accepted: shared definitions live inside the schema
let local = TypeSchema::new(
"payment_initiation",
json!({
"$defs": {"amount": {"type": "string", "pattern": "^[0-9]+(\\.[0-9]{2})?$"}},
"type": "object",
"properties": {
"instructedAmount": {
"type": "object",
"properties": {"amount": {"$ref": "#/$defs/amount"}}
}
}
}),
"Payment initiation",
);
assert!(TypeRegistry::new().with_type(local).is_ok());
}
Error handling
Validation returns Result<_, ValidationError>:
| Variant | Meaning |
|---|---|
UnknownType { type_id } | The detail names a type that is not registered (or does not match the schema) |
SchemaValidationFailed(String) | The detail object violates the JSON Schema; the message describes the violation |
InvalidDetail(String) | The detail is not a JSON object, carries a conflicting type, or the request JSON is malformed |
InvalidSchema(String) | A JSON Schema failed to compile or references an external document (at registration) |
InvalidComparator(String) | A comparator rule does not name a single field (at registration) |
#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
let payment_schema = TypeSchema::new(
"payment_initiation",
serde_json::json!({
"type": "object",
"required": ["actions", "instructedAmount"]
}),
"Payment initiation authorization",
);
let detail = AuthorizationDetail::new(
"payment_initiation",
serde_json::json!({
"actions": ["initiate"],
"instructedAmount": {"currency": "EUR", "amount": "123.50"}
}),
);
match validate_against_schema(&detail, &payment_schema) {
Ok(()) => { /* accept the detail */ }
Err(ValidationError::UnknownType { type_id }) => {
println!("Unknown type: {}", type_id);
}
Err(ValidationError::SchemaValidationFailed(msg)) => {
println!("Malformed detail: {}", msg);
}
Err(other) => {
panic!("Registry misconfiguration: {}", other);
}
}
}
Best practices
- Register every type your server understands, with a description, before validating requests. The registry is your source of truth for what the server will ever accept.
- Keep JSON Schemas tight:
requiredfields,enumlists for closed vocabularies (likeactions), and patterns for identifiers (IBANs, ISO 4217 currency codes). Validation is the first line of defense against malformed requests. - Keep schemas self-contained: inline shared definitions with
$defsrather than referencing URLs or files, which are rejected. - Build the
TypeRegistryat startup:InvalidSchemaandInvalidComparatorthen surface as configuration errors before the first request, and every request reuses the compiled schemas.
Next steps
With well-formed details in hand, you can enforce field-level limits with Rules and Policies and guarantee at the token endpoint that re-requests only narrow the original grant using Subset Comparison.