Overview
This example demonstrates the polymorphic type system in Tesseract's PropertyTree, which enables schema validation for type hierarchies where fields accept a base type or any registered derived type.
Polymorphic types are useful for:
- Type Inheritance: Accept base class or any subclass
- Plugin Architectures: Accept base plugin type or derived implementations
- Constraint Systems: Accept base constraint or specific constraint types
- Extensibility: New derived types can be registered without schema changes
- Dynamic Dispatch: Configuration data includes "type" field for validation selection
Key Concepts
- Base Type: A registered type that other types can derive from
- Derived Type: A type that extends the base type and is registered as such
- Type Registration: Using TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE macro
- Type Field: A required field in configuration specifying the concrete type
- Polymorphic Validation: Field marked with acceptsDerivedTypes() accepts any registered derived type
- Type Safety: Validation checks if the type is valid in the hierarchy
Polymorphic Validation Workflow
The example demonstrates:
- Register Base Schema: Define the base type schema
- Register Derived Schemas: Define schemas for each derived type
- Register Inheritance: Tell registry which types derive from base
- Mark Field: Use acceptsDerivedTypes() on fields accepting polymorphic types
- Validate: System checks "type" field is valid and validates against corresponding schema
Example Code
auto base_constraint_schema = PropertyTreeBuilder()
.attribute(TYPE, CONTAINER)
.doubleNum("weight").defaultVal(1.0).minimum(0.0).doc("Constraint weight in optimization").done()
.build();
auto position_constraint_schema = PropertyTreeBuilder()
.attribute(TYPE, CONTAINER)
.doubleNum("weight").defaultVal(1.0).minimum(0.0).doc("Constraint weight in optimization").done()
.doubleNum("target_x").required().doc("Target position X coordinate").done()
.doubleNum("target_y").required().doc("Target position Y coordinate").done()
.doubleNum("target_z").required().doc("Target position Z coordinate").done()
.doubleNum("target_tolerance").defaultVal(0.01).minimum(0.0).doc("Position tolerance in meters").done()
.build();
auto orientation_constraint_schema = PropertyTreeBuilder()
.attribute(TYPE, CONTAINER)
.doubleNum("weight").defaultVal(1.0).minimum(0.0).doc("Constraint weight in optimization").done()
.doubleNum("target_roll").required().doc("Target roll angle in radians").done()
.doubleNum("target_pitch").required().doc("Target pitch angle in radians").done()
.doubleNum("target_yaw").required().doc("Target yaw angle in radians").done()
.doubleNum("target_tolerance").defaultVal(0.1).minimum(0.0).doc("Orientation tolerance in radians").done()
.build();
registry->registerSchema("Constraint", base_constraint_schema);
registry->registerSchema("PositionConstraint", position_constraint_schema);
registry->registerSchema("OrientationConstraint", orientation_constraint_schema);
registry->registerDerivedType("Constraint", "PositionConstraint");
registry->registerDerivedType("Constraint", "OrientationConstraint");
First, register the type hierarchy. Base types and derived types each get schemas. Then use TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE to establish relationships.
auto trajectory_schema = PropertyTreeBuilder()
.attribute(TYPE, CONTAINER)
.string("name").required().doc("Name of this trajectory").done()
.container("constraint").doc("Task constraint (any constraint type can be used)")
.customType("constraint", "Constraint")
.acceptsDerivedTypes()
.validator(validateCustomType)
.done()
.build();
Mark sequence/map fields with acceptsDerivedTypes() to allow derived types. The validator will check each element's "type" field against the hierarchy.
YAML::Node valid_config;
valid_config["name"] = "approach_trajectory";
valid_config["constraint"]["type"] = "PositionConstraint";
valid_config["constraint"]["weight"] = 2.0;
valid_config["constraint"]["target_x"] = 1.0;
valid_config["constraint"]["target_y"] = 2.0;
valid_config["constraint"]["target_z"] = 3.0;
valid_config["constraint"]["target_tolerance"] = 0.005;
auto schema_copy = trajectory_schema;
schema_copy.mergeConfig(valid_config);
auto errors = schema_copy.validate();
During validation, the system checks if each element's type is registered as derived from (or equal to) the base type, then validates against that type's schema.
Comparing Polymorphism Approaches
PropertyTree offers two polymorphism mechanisms:
| Feature | Derived Types | OneOf |
| Selection Method | Type field (explicit naming) | Required field match (implicit) |
| Type Discovery | "type": "DerivedClassName" | Keys present in config determine branch |
| Registration | Runtime via macros (compile-time execution) | Static in schema definition |
| Extensibility | Very extensible - new types can be added | Requires schema modification |
| Type Hierarchies | Supports inheritance chains | Not designed for hierarchies |
| Error Messages | Clear type name in error | Shows expected/actual keys |
| Typical Use Case | Plugins, constraints, different algorithm implementations | Different communication methods, |
shape types |
Best Practices
- Always Include Type Field: Polymorphic fields need explicit type information
- Use Clear Type Names: Make type names match class names when possible
- Register Early: Use TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE at module init time
- Document Type Hierarchy: Add doc attributes explaining what types are valid
- Provide Type Defaults: Consider if a default type makes sense for the field
- Validate Complete Config: Always call validate() and check all error messages
- Consider Schema Versioning: Plan for adding new derived types in the future
- Test All Types: Write validation tests covering each derived type variant
Running the Example
Execute the example:
./tesseract_common_polymorphic_property_tree_example
The example demonstrates:
- Registering type hierarchies
- Validating polymorphic sequences with multiple types
- Error handling for invalid types
- Accessing derived type instances
- Schema inspection with metadata