pxr/usdValidation/usdValidation/README.md
The OpenUSD Validation framework provides a system to validate assets, verifying core rules, schema rules, and client-provided rules via plugins, to ensure assets are robust and interchangeable between different USD workflows.
[UsdValidationValidators](@ref UsdValidationValidator) are used to run validation tests. A single UsdValidationValidator instance represents a single validation test that can result in zero or more named validation errors when run. For example a "usdValidation:CompositionErrorTest" validator might test for multiple types of composition errors, returning errors for any composition issues it encounters.
Each Validator instance has metadata represented by UsdValidationValidatorMetadata. Validator metadata includes:
Validator instances can be used to run validation tests, but more commonly a set of validators will be used, represented by a UsdValidationContext. A UsdValidationContext can be created from a vector of UsdValidators, which can be created manually, or obtained via metadata query methods on UsdValidationRegistry. UsdValidationContext also provides convenience constructors that determine which validators to use based on metadata filters, such as a list of keywords.
Some constructors allow for including validators for ancestor schema types, for
any found validators associated with schema types. For example, when using a
UsdValidationContext constructor with the keywords parameter, if
includeAllAncestors is set to true (the default), and a validator is found
for, say, the UsdGeomSphere schema type, any validators associated with ancestor
schema types of UsdGeomSphere (such as UsdGeomGprim, UsdGeomImageable, etc.)
will also be included in the context.
UsdValidationRegistry is the central registry that manages all registered validators. The registry is used to obtain validator instances via validator metadata (name, keywords, schemaTypes). The registry also provides access to registered [UsdValidationValidatorSuites](@ref UsdValidationValidatorSuite) which represent predefined sets of validators (that can be used to create a UsdValidationContext). Finally, the registry can be used to register custom validators (see "Creating Custom Validators" below).
Validator instances (as well as the UsdValidationRegistry singleton) are immutable, non-copyable, and (if the validator is registered in the registry) immortal throughout a given USD session.
When validation tests are run (see "Running Validator Tests" below), any errors are captured in [UsdValidationErrors](@ref UsdValidationError). Errors contain the following information:
A UsdValidationFixer represents a fix that can be applied to fix specific validation errors. A fixer is associated with a specific validator, and can be associated with a specific error name, or can be generic to any error associated with the corresponding validator. A fixer contains a name (unique among all fixers associated with a specific validator), description, keywords used to filter/group fixers (e.g. by department, show, etc.), and implementations of FixerImplFn and FixerCanApplyFn functions to apply a fix and verify if a fix can be applied respectively.
Validation tests can be run on a stage, layer, or prim, by using the various
Validate() methods on UsdValidationValidator or UsdValidationContext. When
validating using a UsdValidationContext, multiple UsdValidationValidator
tests will be run in parallel.
Validation tests can potentially initiate stage traversal, and it's the caller's
responsibility to maintain the lifetime of the stage/layer/prims that are being
validated during the lifetime of the validation tests. UsdValidationContext
provides Validate() methods for validating stages that can take a
Usd_PrimFlagsPredicate to control stage traversal.
Validation tests that test time-dependent values will by default be run against
the GfInterval::GetFullInterval() (-inf to inf) time interval. There are
Validate() methods that can take a specific time interval to run against,
and will run tests on all timeCodes in the given time interval.
When validation tests have finished running, any validation errors will be returned as [UsdValidationErrors](@ref UsdValidationError). See details above for information contained in a UsdValidationError. If the error provides associated [UsdValidationFixers](@ref UsdValidationFixer), it is the responsibility of the caller to fix errors using the fixer's [CanApplyFix()](@ref UsdValidationFixer::CanApplyFix()) and [ApplyFix()](@ref UsdValidationFixer::ApplyFix()) methods on the client provided UsdEditTarget. Validation tests will not automatically call any fixers.
Custom validators can be created either via the OpenUSD plugin infrastructure, which results in lazy loading of the validators, or via explicitly creating and registering validators via UsdValidationValidator and UsdValidationRegistry APIs.
A custom validator must implement a validator task function ([UsdValidateLayerTaskFn](@ref UsdValidateLayerTaskFn()), [UsdValidateStageTaskFn](@ref UsdValidateStageTaskFn()), or [UsdValidatePrimTaskFn](@ref UsdValidatePrimTaskFn())) which gets passed to the registry during registration, and called when the validator's test is run. Validators should implement the task function at the appropriate granularity level. For example, if the validation logic can be succinctly defined to be applied to a prim, implement UsdValidatePrimTaskFn rather than UsdValidateStageTaskFn or UsdValidateLayerTaskFn. Using too broad a granularity can impact performance — for instance, a validator task that only needs to operate at the prim level but is implemented as a stage task may incur unnecessary stage traversal each time it runs.
Similarly, when a prim-level validator only applies to prims of a specific
schema type, ensure schemaTypes is set appropriately. This allows the
validation framework to skip non-matching prims and avoid unnecessary
validator invocations.
For custom validators created in a plugin, the plugin's plugInfo.json will
contain the custom validator metadata. For example, a plugInfo.json for a
plugin that has a "Validator1" validator, and a "ValidatorSuite1" validator
suite, might look something like the following.
{
"Plugins": [
{
"Info": {
"Validators": {
"keywords" : ["commonKeyword"],
"Validator1": {
"doc": "Validator that has test for imageable Gprims.",
"schemaTypes": [
"UsdGeomImageable"
],
"keywords": [
"UsdGeomImageable",
"keyword1"
]
},
"ValidatorSuite1": {
"doc": "Suite of validators",
"keywords": ["suite"],
"isSuite": true
}
}
},
"LibraryPath": "@PLUG_INFO_LIBRARY_PATH@",
"Name": "newValidatorPlugin",
"ResourcePath": "@PLUG_INFO_RESOURCE_PATH@",
"Root": "@PLUG_INFO_ROOT@",
"Type": "library"
}
]
}
Note how the validator metadata is set in the plugInfo.json, along with an
extra "keywords" entry for keywords that are added to all validators
defined in the plugin.
The plugin code to implement and register the validator might look something like the following.
TF_REGISTRY_FUNCTION(UsdValidationRegistry)
{
UsdValidationRegistry ®istry = UsdValidationRegistry::GetInstance();
const TfToken validatorName("newValidatorPlugin:Validator1");
// Create our validator UsdValidateStageTaskFn here
// (you could also use a static function defined elsewhere)
const UsdValidateStageTaskFn stageTaskFn =
[](const UsdStagePtr &usdStage,
const UsdValidationTimeRange &timeRange) {
UsdValidationErrorVector errors;
// ...Validator test logic here, accessing usdStage as needed,
// creating errors as needed...
return errors;
};
registry.RegisterPluginValidator(validatorName, stageTaskFn);
// Register the validator suite to include the validator we just registered
// (in practice, suites will most likely contain more than one validator).
const TfToken suiteName("newValidatorPlugin:ValidatorSuite1");
const std::vector<const UsdValidationValidator *> containedValidators
= registry.GetOrLoadValidatorsByName({ validatorName });
registry.RegisterPluginValidatorSuite(suiteName, containedValidators);
}
Note that UsdValidationError instances are typically created by the validation task functions.
For custom validators created explicitly, create a UsdValidationValidatorMetadata with the desired validator metadata along with the validator test implementation, and use UsdValidationRegistry::RegisterValidator(). The following example creates a UsdValidateStageTaskFn and UsdValidationValidatorMetadata to register an explicit validator.
const UsdValidateStageTaskFn explicitStageTaskFn =
[](const UsdStagePtr &usdStage,
const UsdValidationTimeRange &timeRange) {
UsdValidationErrorVector errors;
// ...Validator test logic here, accessing usdStage as needed,
// creating errors as needed...
return errors;
};
const UsdValidationValidatorMetadata explicitValidatorMetadata = {
TfToken("ExplicitValidator"),
// ...other metadata fields...
};
registry.RegisterValidator(explicitValidatorMetadata, explicitStageTaskFn);
The decision comes down to how you want other code to discover your validator:
| Explicit | Plugin | |
|---|---|---|
| Metadata source | Caller provides ValidatorMetadata | plugInfo.json |
| Discoverability | Only after registration code has run | Metadata visible at startup; validator lazily loaded |
| Lazy loading | No; must register each session | Yes; validator loaded when first queried by name |
| Use when | Prototyping, one-off scripts, tests, runtime-generated rules | Shipping validators in a distributed plugin |
If other code needs to query your validator's metadata (keywords, schema types) before the validator is loaded, use plugin registration. If the validator is created dynamically or is only used by the code that creates it, explicit registration is simpler.
UsdValidationRegistry::RegisterPluginValidator() and UsdValidationRegistry::RegisterValidator() can optionally take a vector of [UsdValidationFixers](@ref UsdValidationFixer). Each fixer will specify a name, description, FixerCanApplyFn and FixerImplFn functions, a list of keywords, and the error name the fixer can fix.
Validator tests can result in multiple errors, and multiple fixers may be associated with some of these errors. UsdValidationFixer::CanApplyFix() will utilize all of this information to determine if a fixer can be applied.
The following example shows a utility function that creates a new fixer, adds it to a vector of fixers, and returns the vector.
const std::vector<UsdValidationFixer>
_ValidatorFixers() {
std::vector<UsdValidationFixer> fixers;
FixerCanApplyFn fixerCanApplyFn =
[](const UsdValidationError &error, const UsdEditTarget &editTarget,
const UsdTimeCode &/*timeCode*/) -> bool {
// ...fixer logic here...
return true;
};
FixerImplFn fixerImplFn =
[](const UsdValidationError &error, const UsdEditTarget &editTarget,
const UsdTimeCode &/*timeCode*/) -> bool {
// ...can apply fixer logic here...
return true;
};
fixers.emplace_back(
TfToken("Example Fixer"),
"An example fixer.",
fixerImplFn, fixerCanApplyFn, TfTokenVector{},
TfToken("ErrorNameAssociatedWithFixer"));
return fixers;
}
Pass in the vector of fixers when the validator is registered. For example, the registration code to register a plugin validator with fixers, using the previously shown utility function, might look something like the following.
registry.RegisterPluginValidator(validatorName, stageTaskFn, _ValidatorFixers());
Note that UsdValidationRegistry does not manage fixers directly, and these are held by their respective UsdValidationValidator(s).
Custom validators can be implemented in Python using either of the two registration paths described in "Creating Custom Validators":
Plugin registration (RegisterPluginLayerValidator,
RegisterPluginStageValidator, RegisterPluginPrimValidator) —
the caller provides only the validator name as a TfToken.
Metadata comes from the plugin's plugInfo.json and is parsed
automatically during registry initialization.
Explicit registration (RegisterLayerValidator,
RegisterStageValidator, RegisterPrimValidator) — the caller
provides full ValidatorMetadata. No plugin infrastructure is
required; the validator is available immediately after registration.
When a ValidationContext runs validators, all validator tasks — C++ and
Python — are dispatched into the same shared TBB worker thread pool. Python
task functions must acquire the Python GIL on each invocation. A TBB worker
thread executing one of these Python validator tasks is blocked waiting for the
GIL. This has two consequences:
Python validators do not benefit from parallelism among themselves. Even with task parallelism available via UsdValidationContext, only one Python validator task runs at a time; the rest are blocked waiting for the GIL.
Python validators can starve C++ validators. If enough Python validator tasks are scheduled simultaneously to occupy all TBB worker threads, C++ validator tasks that are ready to run will sit in the queue with no available workers until a GIL-blocked thread finishes and is freed.
For performance-sensitive validation pipelines, prefer C++ implementations for validators. Python validators are best suited for prototyping, tooling, or validators that run rarely and on small scenes.
Each registration method accepts a Python callable with a specific signature, matching the corresponding C++ task function type:
| Method | Callable signature |
|---|---|
RegisterLayerValidator / RegisterPluginLayerValidator | (layer: Sdf.Layer) -> list[ValidationError] |
RegisterStageValidator / RegisterPluginStageValidator | (stage: Usd.Stage, timeRange: UsdValidation.TimeRange) -> list[ValidationError] |
RegisterPrimValidator / RegisterPluginPrimValidator | (prim: Usd.Prim, timeRange: UsdValidation.TimeRange) -> list[ValidationError] |
The callable must return a list (or any iterable) of
UsdValidation.ValidationError objects. Return an empty list when the
validation passes.
from pxr import Sdf, UsdValidation
registry = UsdValidation.ValidationRegistry()
metadata = UsdValidation.ValidatorMetadata(
name="myPackage:RequiresDefaultPrim",
doc="Warn when a layer has no default prim set.",
keywords=["myPackage"],
)
def _CheckDefaultPrim(layer):
if not layer.defaultPrim:
return [
UsdValidation.ValidationError(
"MissingDefaultPrim",
UsdValidation.ValidationErrorType.Warn,
[UsdValidation.ValidationErrorSite(
layer, Sdf.Path.absoluteRootPath)],
f"Layer '{layer.identifier}' has no defaultPrim.",
)
]
return []
registry.RegisterLayerValidator(metadata, _CheckDefaultPrim)
from pxr import Sdf, Usd, UsdGeom, UsdValidation
registry = UsdValidation.ValidationRegistry()
metadata = UsdValidation.ValidatorMetadata(
name="myPackage:RequiresUpAxis",
doc="Error when a stage has no upAxis metadata.",
keywords=["myPackage"],
)
def _CheckUpAxis(stage, timeRange):
if not stage.HasAuthoredMetadata(UsdGeom.Tokens.upAxis):
return [
UsdValidation.ValidationError(
"MissingUpAxis",
UsdValidation.ValidationErrorType.Error,
[UsdValidation.ValidationErrorSite(
stage, Sdf.Path.absoluteRootPath)],
"Stage is missing upAxis metadata.",
)
]
return []
registry.RegisterStageValidator(metadata, _CheckUpAxis)
from pxr import Sdf, Usd, UsdValidation
registry = UsdValidation.ValidationRegistry()
metadata = UsdValidation.ValidatorMetadata(
name="myPackage:NoPrimsMissingKind",
doc="Warn when a prim has no kind set.",
keywords=["myPackage"],
)
def _CheckKind(prim, timeRange):
if prim.IsPseudoRoot(): # skip pseudo-root
return []
model = Usd.ModelAPI(prim)
if not model.GetKind():
return [
UsdValidation.ValidationError(
"MissingKind",
UsdValidation.ValidationErrorType.Warn,
[UsdValidation.ValidationErrorSite(
prim.GetStage(), prim.GetPath())],
f"Prim '{prim.GetPath()}' has no kind.",
)
]
return []
registry.RegisterPrimValidator(metadata, _CheckKind)
When a validator is declared in plugInfo.json, only the name is
needed at registration time; all other metadata is already known to
the registry.
Given a plugInfo.json that declares:
{
"Plugins": [{
"Info": {
"Validators": {
"CheckUpAxis": {
"doc": "Error when upAxis is missing.",
"keywords": ["stageMetadata"]
}
}
},
"Name": "myPlugin",
"Type": "library",
...
}]
}
The Python implementation registers the task function by name:
from pxr import Sdf, UsdGeom, UsdValidation
registry = UsdValidation.ValidationRegistry()
def _CheckUpAxis(stage, timeRange):
if not stage.HasAuthoredMetadata(UsdGeom.Tokens.upAxis):
return [
UsdValidation.ValidationError(
"MissingUpAxis",
UsdValidation.ValidationErrorType.Error,
[UsdValidation.ValidationErrorSite(
stage, Sdf.Path.absoluteRootPath)],
"Stage is missing upAxis metadata.",
)
]
return []
# Name must match "pluginName:validatorName" from plugInfo.json.
registry.RegisterPluginStageValidator(
"myPlugin:CheckUpAxis", _CheckUpAxis
)
Plugin validator suites work the same way:
registry.RegisterPluginValidatorSuite(
"myPlugin:MySuite",
[registry.GetOrLoadValidatorByName("myPlugin:CheckUpAxis")]
)
For C++ plugins the Plug system loads the shared library and the
TF_REGISTRY_FUNCTION(UsdValidationRegistry) macro ensures the registration
code runs automatically at load time.
Python plugins work the same way, but will rely on module-level registration
code in __init__.py instead of TF_REGISTRY_FUNCTION.
The lazy-load flow for a Python plugin:
Startup: ValidationRegistry parses plugInfo.json for all
discovered plugins. Validator metadata (name, doc, keywords,
schemaTypes) is available immediately, before any code is loaded.
Query: Client code calls
registry.GetOrLoadValidatorByName("myPlugin:CheckUpAxis").
The registry finds metadata for this name and sees it belongs to a plugin
that has not been loaded yet. The same load is triggered when validators are
accessed via UsdValidationContext.
Load: The registry calls plugin->Load(). For a Python-type
plugin, the Plug system executes
import <module_name> (where <module_name> matches the "Name"
field in plugInfo.json).
Register: The module's __init__.py runs at import time.
Its top-level code calls RegisterPluginStageValidator (or
the layer/prim variant) to register task functions with the
registry.
Return: The registry now has a fully registered validator and returns it to the caller.
The module directory name must match the "Name" field in
plugInfo.json, and the plugInfo.json lives inside the module
directory alongside __init__.py:
myPlugin/
__init__.py # Registration code runs at import time
plugInfo.json # "Type": "python", "Name": "myPlugin"
{
"Plugins": [{
"Type": "python",
"Name": "myPlugin",
"Info": {
"Validators": {
"CheckUpAxis": {
"doc": "Error when upAxis is missing.",
"keywords": ["stageMetadata"]
}
}
}
}]
}
from pxr import Sdf, UsdGeom, UsdValidation
_PLUGIN_NAME = "myPlugin"
def _CheckUpAxis(stage, timeRange):
if not stage.HasAuthoredMetadata(UsdGeom.Tokens.upAxis):
return [
UsdValidation.ValidationError(
"MissingUpAxis",
UsdValidation.ValidationErrorType.Error,
[UsdValidation.ValidationErrorSite(
stage, Sdf.Path.absoluteRootPath)],
"Stage is missing upAxis metadata.",
)
]
return []
# Registration at import time — equivalent to TF_REGISTRY_FUNCTION
_registry = UsdValidation.ValidationRegistry()
_registry.RegisterPluginStageValidator(
_PLUGIN_NAME + ":CheckUpAxis", _CheckUpAxis)
The plugin directory must be discoverable by Plug.Registry (either
on PXR_PLUGINPATH_NAME or registered via
Plug.Registry().RegisterPlugins()). The parent directory of the
module must be on sys.path so the import succeeds.
Retrieve the registered validator by name and call Validate(), or
pass it to a ValidationContext to run it alongside other validators.
# Direct invocation
validator = registry.GetOrLoadValidatorByName(
"myPackage:RequiresUpAxis"
)
stage = Usd.Stage.Open("asset.usda")
errors = validator.Validate(stage)
for error in errors:
print(error.GetErrorAsString())
# Via ValidationContext (runs all provided validators in parallel)
context = UsdValidation.ValidationContext([validator])
errors = context.Validate(stage)
stage_validator = registry.GetOrLoadValidatorByName(
"myPackage:RequiresUpAxis"
)
prim_validator = registry.GetOrLoadValidatorByName(
"myPackage:NoPrimsMissingKind"
)
suite_metadata = UsdValidation.ValidatorMetadata(
name="myPackage:BaselineChecks",
doc="Suite of baseline asset checks.",
keywords=["myPackage"],
isSuite=True,
)
registry.RegisterValidatorSuite(
suite_metadata, [stage_validator, prim_validator]
)
ValidationRegistry is a singleton; validators registered in
one module are visible to all other modules in the same process.HasValidator() to check
before registering if needed.plugInfo.json.
The metadata is discoverable at startup even before the Python task
function is registered, enabling tools to enumerate available
validators without loading every plugin.The code for usdchecker (in pxr/usdValidation/bin/usdchecker) has been
updated to use validators and provides additional examples for using the
validation framework.
See the various schema validators in /pxr/usdValidation for more example validator plugins.