Validation¶
waku validates module configuration at startup to catch wiring errors early, before your application begins serving requests. Misconfigured imports, missing exports, or inaccessible dependencies are detected and reported immediately — either as hard failures or warnings — so you never encounter cryptic runtime resolution errors in production.
Validation runs as an AfterApplicationInit hook, meaning it executes after the
dependency injection container is fully built. At that point, every module, provider,
and import/export relationship is finalized and available for inspection.
ValidationExtension¶
ValidationExtension is the engine that drives startup validation. It is included in
DEFAULT_EXTENSIONS and enabled by default — every application created through
WakuFactory benefits from validation without any extra configuration.
Default Setup¶
WakuFactory includes ValidationExtension in DEFAULT_EXTENSIONS automatically — no
configuration needed:
Constructor¶
| Parameter | Type | Default | Description |
|---|---|---|---|
rules |
Sequence[ValidationRule] |
(required) | Validation rules to execute |
strict |
bool |
True |
Fail fast or warn |
Strict vs. Lenient Mode¶
Raises an ExceptionGroup containing all ValidationError instances.
The application fails to start.
Tip
Use strict=False during migration or prototyping when you want visibility into
violations without blocking startup. Switch back to strict=True before deploying
to production.
DependenciesAccessibleRule¶
DependenciesAccessibleRule is the built-in rule shipped with waku. It verifies that
every dependency required by every provider is reachable through the module's
import chain.
What It Checks¶
For each module, the rule iterates over every provider's factory dependencies and confirms that each dependency type is accessible to the module. Accessibility is determined through four strategies, checked in order:
- Global providers — the type is provided by a global module (
is_global=True) or registered as anAPP-scoped context variable. - Local providers — the type is provided within the same module.
- Context variables — the type is a context variable registered on the module.
- Imported modules — the type is exported by a module that the current module imports (direct export or re-export).
If none of these strategies match, the dependency is flagged as inaccessible.
Example Violation¶
Consider two modules where OrderService depends on PaymentService, but
OrderModule does not import the module that provides it:
At startup, DependenciesAccessibleRule detects that PaymentService is not
accessible to OrderModule and produces a DependencyInaccessibleError:
Dependency Error: "<class 'PaymentService'>" is not accessible
Required by: "<class 'OrderService'>"
In module: "OrderModule"
To resolve this issue, either:
1. Export "<class 'PaymentService'>" from a module that provides it and add that module to "OrderModule" imports
2. Make the module that provides "<class 'PaymentService'>" global by setting is_global=True
3. Move the dependency to a module that has access to "<class 'PaymentService'>"
Note: Dependencies can only be accessed from:
- The same module that provides them
- Modules that import the module that provides and exports it
- Global modules
How to Fix¶
The error message tells you exactly what's missing. In this case, OrderModule needs
access to PaymentService. You have two options:
Import the module that provides it — if PaymentService is scoped to specific
consumers:
Make PaymentModule global — if PaymentService is a shared service used across
many modules:
@module(
providers=[scoped(PaymentService)],
exports=[PaymentService],
is_global=True,
)
class PaymentModule: ...
Warning
Global modules reduce boilerplate but weaken encapsulation. Reserve them for truly cross-cutting infrastructure — database connections, configuration, logging. Feature modules should use explicit imports to keep their dependency graph visible.
Custom Validation Rules¶
You can implement your own rules to enforce project-specific conventions at startup.
Custom rules are extensions — they
hook into the application lifecycle just like the built-in ValidationExtension.
The ValidationRule Protocol¶
Every rule implements a single method — validate(context) -> list[ValidationError].
The ValidationContext provides access to the fully initialized WakuApplication,
including its module registry (context.app.registry) and DI container
(context.app.container).
Example: Custom Rule¶
The following rule enforces that every non-root module has at least one export, preventing modules that provide services but forget to expose them:
Registering Custom Rules¶
Pass your custom rules alongside the built-in ones when constructing
ValidationExtension:
Then pass the extension to WakuFactory:
Disabling Validation¶
To disable all default extensions, including validation, pass an empty sequence
to WakuFactory:
To disable only ValidationExtension while keeping other extensions, construct a
custom extensions list:
Warning
Disabling validation removes the safety net that catches import/export wiring errors at startup. Only disable it when you have a specific reason, such as running a minimal test harness or during early prototyping.
Further reading¶
- Modules — module system, imports, and export boundaries
- Custom Extensions — writing your own extensions and hooks
- Lifecycle Hooks — when validation runs in the application lifecycle
- Testing — test utilities and working with validation in tests