Validating ODM Documents
odmlib provides several validation mechanisms for ODM documents.
Element Order Verification
ODM specifies the required order of child elements in XML.
Note
write_xml() and to_xml() always emit children in the model
declaration order defined in the model module (e.g.
odmlib/odm_1_3_2/model.py), regardless of the order in which you
assigned the children on the instance. You do not need to call
reorder_object() before serializing — the serializer drives the
order from the metaclass-captured _elems, which mirrors the
schema content model.
The verify_order() / reorder_object() API is still useful when
you want to fail-fast on object construction order (e.g. so unit tests
catch a stale build script that assigns children out of order), or when
you mutate the in-memory __dict__ ordering for some other reason.
odmlib can verify and fix element ordering:
import warnings
from odmlib.exceptions import OdmlibElementOrderError
# Check order (raises OdmlibElementOrderError if out of order)
try:
odm.verify_order()
print("Element order is correct")
except OdmlibElementOrderError as e:
print(f"Order error: {e}")
# Fix automatically (issues OdmlibWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
odm.reorder_object()
Scope and limitations
verify_order() is a method on ODMElement,
so it works on any odmlib model object — the ODM root, Study,
MetaDataVersion, ItemGroupDef, leaf elements, and so on. It
recurses through every ODMElement child and every entry of every
ODMListObject list, so a single call on the odmlib root covers the
whole document.
It does not work on xml.etree.ElementTree.Element — that
is the parsed XML representation, not an odmlib object. If you only
have an ElementTree, load it through
XMLODMLoader (or
XMLDefineLoader for Define-XML) first.
A few limitations to keep in mind:
Custom extensions must redeclare base children. Each class’s child-element ordering comes from the descriptors declared in its own class body, captured into
_elemsbyODMMeta. If you subclass an existing model class to extend it, you must redeclare the base children in schema order — seeodmlib/define_2_1/model.pyfor the canonical pattern (theItemGroupDefextension assignsDescription = ODM.ItemGroupDef.Descriptionetc. to lock in the order). If a base child is missing from the subclass body,verify_order()will treat it as unexpected.Permissive mode can produce false positives. When loading a non-conformant document with
SKIP_TYPEactive, unknown attributes are accepted into the instance__dict__directly. Those keys are not in_elemsand will tripverify_order(). Run order verification only after you have cleaned up any unknown-attribute artefacts. See Permissive Loading Mode for the load-fix-validate workflow.List-internal order is not checked.
verify_order()only checks the order of different kinds of child elements relative to each other (e.g.,DescriptionbeforeItemRef). It does not check whether twoItemRefentries inside anItemGroupDefappear in the right order — ODM typically relies onOrderNumberattributes for that, not list position.Completeness is not checked.
verify_order()does not detect missing required children. Useverify_conformance()(Cerberus schema check) for that, orvalidate()to combine both.
Relationship to serialization
Different serializers handle child order differently:
to_xml()/write_xml()emit children in_elems(model declaration) order regardless of__dict__insertion order. You do not need to run ``verify_order()`` / ``reorder_object()`` before XML serialization.to_dict()/to_json()walk__dict__directly, so they reflect whatever order the instance accumulated. If a stable, schema-aligned key order matters for downstream consumers of your JSON/dict output, runverify_order()(andreorder_object()if it fails) before calling them.
OID Uniqueness and Reference Integrity
odmlib can verify that all OIDs are unique and that all OID references point to valid definitions:
Using the Dynamic OID Checker (recommended)
from odmlib import create_oid_checker
# Create a dynamic checker for ODM 1.3.2
checker = create_oid_checker("odm_1_3_2")
# Verify OIDs (raises OdmlibOIDError on failure)
is_valid = odm.verify_oids(checker)
print(f"OIDs valid: {is_valid}")
# Find unreferenced definitions (e.g., unused code lists)
unreferenced = odm.unreferenced_oids(checker)
for oid in unreferenced:
print(f"Unreferenced: {oid}")
Using the Manual OID Checker (deprecated)
import odmlib.odm_1_3_2.rules.oid_ref as OR
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
checker = OR.OIDRef()
is_valid = odm.verify_oids(checker)
Conformance Validation
Cerberus-based schema validation checks structural conformance:
import odmlib.odm_1_3_2.rules.metadata_schema as SC
validator = SC.MetadataSchema()
result = mdv.verify_conformance(validator)
print(f"Conformance result: {result}")
Combined Validation with Error Collection
Use the validate() method to run all checks and collect every error
instead of failing on the first one:
from odmlib import create_oid_checker
import odmlib.odm_1_3_2.rules.metadata_schema as SC
checker = create_oid_checker("odm_1_3_2")
validator = SC.MetadataSchema()
errors = odm.validate(
collect_errors=True,
oid_checker=checker,
conformance_checker=validator
)
if not errors:
print("Document is valid")
else:
for err in errors:
print(f"Error: {err}")
What “every error” means per layer
Each of the three layers enumerates its own problems rather than stopping at the first:
Element order — one
OdmlibElementOrderErrorper misordered element. The walk continues into the children of a misordered element, so a single pass lists everythingreorder_object()needs to fix.OID — one
OdmlibOIDErrorper duplicate OID and per bad reference. Duplicates no longer abort the traversal, so the reference checks still run. On a duplicate the first definition is kept.Conformance — the bundled Cerberus result is expanded into one
OdmlibConformanceErrorper failing field, each carrying a dottedfield_path(e.g."ItemGroupDef.0.Name") and the complete raw dict oncerberus_errors.
Because the number of errors is now unbounded, errors are best filtered by type rather than accessed by index:
from odmlib import OdmlibOIDError
oid_problems = [e for e in errors if isinstance(e, OdmlibOIDError)]
Capping the error list
A badly broken document can produce thousands of errors. max_errors stops
collection once the cap is reached and appends a final
OdmlibErrorLimitError, so the list holds at most
max_errors + 1 entries:
errors = odm.validate(collect_errors=True, oid_checker=checker,
max_errors=100)
if errors and isinstance(errors[-1], OdmlibErrorLimitError):
print("More problems remain — fix these and re-run")
The cap is enforced inside each layer, so validation genuinely stops early rather than collecting everything and truncating the result.
Checker reuse
An OID checker accumulates every OID it sees, so it is single-use per
document. Reusing one reports every OID as a duplicate. Either build a fresh
checker per document, or call reset():
checker.reset()
errors = other_odm.validate(collect_errors=True, oid_checker=checker)
validate() emits an OdmlibWarning in collect
mode when it is handed a checker that still holds state.
Note
The deprecated rules/oid_ref.py OIDRef classes (removed in v0.3.0)
do not implement the collecting protocol and contribute at most one
error for the whole OID layer. Use
create_oid_checker() to get full reporting.
is_collecting_checker() tells you which you have.
Only OdmlibError subclasses are collected. Any
other exception — a bug in a custom checker, an AttributeError from a
malformed tree — propagates in both modes rather than being reported as a
document defect.
Collecting from verify_oids() directly
The OID layer can be run on its own with collection enabled:
from odmlib import ErrorCollector
collector = ErrorCollector()
with checker.collecting(collector):
odm.verify_oids(checker)
for err in collector.errors:
print(err)
This pattern is particularly useful after permissive loading, where a
non-conformant document has been loaded with validation bypassed. After
fixing known issues, use validate(collect_errors=True) to confirm all
problems have been resolved. See Permissive Loading Mode for the
complete workflow.
XML Schema (XSD) Validation
Use ODMSchemaValidator to validate an
ODM/Define-XML document against the official CDISC XSD bundled with
odmlib:
from odmlib.odm_parser import ODMSchemaValidator, ODMParser
# ODM 1.3.2
validator = ODMSchemaValidator(standard="odm", version="1.3.2")
# ODM 2.0
validator = ODMSchemaValidator(standard="odm", version="2.0")
# Define-XML 2.0 / 2.1
validator = ODMSchemaValidator(standard="define", version="2.1")
# ARM 1.0 (Analysis Results Metadata) in a Define-XML 2.1 document
validator = ODMSchemaValidator(standard="arm", version="1.0-define2.1")
# Validate a file directly (raises OdmlibSchemaValidationError on failure)
validator.validate_file("study.xml")
# Or validate an already-parsed ElementTree (returns bool)
tree = ODMParser("study.xml").parse_tree()
is_valid = validator.validate_tree(tree)
Both standard and version are required; there is no default.
Validating ARM (Analysis Results Metadata)
ARM 1.0 is an extension embedded in a Define-XML document rather than a standalone document type, so odmlib bundles two ARM schema sets — one per Define-XML version:
|
Validates |
|---|---|
|
ARM 1.0 inside a Define-XML 2.0 document (the CDISC original) |
|
ARM 1.0 inside a Define-XML 2.1 document |
The two are not interchangeable: Define-XML 2.0 and 2.1 use different
def: namespace URIs, so the wrong pairing rejects the document on the
first def:-prefixed attribute it encounters. Use "1.0-define2.1"
with odmlib.arm_1_0, which extends odmlib.define_2_1.
Both ARM schemas are supersets of their base Define-XML schema, so either one also validates an ARM-free Define-XML document of the matching version.
For a custom or local XSD that is not bundled with odmlib, pass an
explicit xsd_file path:
validator = ODMSchemaValidator(xsd_file="/path/to/custom.xsd")
The supported (standard, version) combinations are listed in
odmlib.schema_manager._MAIN_SCHEMA.
Understanding Error Types
odmlib uses a custom exception hierarchy:
Exception |
When raised |
|---|---|
|
Base class for all odmlib errors |
|
Type validation failure (wrong type assigned to attribute) |
|
Value validation failure (invalid format, out of range) |
|
Required attribute missing during construction |
|
Element order does not match ODM specification |
|
OID uniqueness or ref/def integrity failure |
|
Loader used before document opened |
|
Invalid namespace URI or unregistered prefix |