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

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>:

VariantMeaning
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: required fields, enum lists for closed vocabularies (like actions), 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 $defs rather than referencing URLs or files, which are rejected.
  • Build the TypeRegistry at startup: InvalidSchema and InvalidComparator then 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.