Comprehensive guide to using derived types with the plugin info structure.
Overview
PropertyTree provides comprehensive support for derived types in configuration validation. This feature enables extensible, inheritance-based configuration hierarchies where fields can accept any properly registered derived type of a base type.
Key Features
- Heterogeneous Collections: Arrays and maps can contain different derived types
- Dynamic Registration: New types can be registered at runtime without schema changes
- Plugin Pattern: Uses the standard Tesseract "class" + "config" plugin info structure
- Type Safety: Validation ensures types are properly registered and configs are valid
- Clear Error Messages: Reports invalid types with available options
The implementation uses the standard Tesseract plugin info structure (class + config) to cleanly separate type identification from configuration:
field:
class: DerivedTypeIdentifier # Type identifier (required)
config: # Type-specific config (optional)
param1: value1
param2: value2
This aligns with the PluginInfo struct used throughout Tesseract for extensible components.
Quick Start
Here's the three-step process to use derived types:
Step 1: Register Types
PropertyTreeBuilder().container("base_constraint").done().build()
);
PropertyTreeBuilder()
.container("jp_constraint")
.string("joint").required().done()
.doubleNum("position").required().done()
.done()
.build()
);
#define TESSERACT_SCHEMA_REGISTER(KEY, SCHEMA_SOURCE)
Macro to register either a file‐based schema or a function‐built schema.
Definition schema_registration.h:64
#define TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE(BASE_TYPE, DERIVED_TYPE)
Macro to register that a derived type can be used where a base type is expected.
Definition schema_registration.h:85
Step 2: Define Schema with acceptsDerivedTypes()
auto schema = PropertyTreeBuilder()
.container("constraints")
.customType("list", "List[BaseConstraint]")
.acceptsDerivedTypes()
.done()
.done()
.build();
Step 3: Use Plugin Info Structure in YAML
constraints:
- class: JointPositionConstraint
config:
joint: "joint_1"
position: 0.785
- class: BaseConstraint
config:
weight: 1.0
- That's it! The schema will validate that each element has a valid class name and config values.
Common Patterns
Single Entry
.customType("constraint", "BaseConstraint")
.acceptsDerivedTypes()
constraint:
class: JointPositionConstraint
config:
joint: "joint_1"
position: 0.785
Array/List
.customType("constraints", "List[BaseConstraint]")
.acceptsDerivedTypes()
constraints:
- class: JointPositionConstraint
config: { joint: "joint_1", position: 0.0 }
- class: CollisionConstraint
config: { constraint_type: "USE_LIMITS" }
Map
.customType("named_constraints", "Map[String,BaseConstraint]")
.acceptsDerivedTypes()
named_constraints:
home_position:
class: JointPositionConstraint
config: { joint: "wrist_3", position: 1.57 }
safety_limit:
class: CollisionConstraint
config: { constraint_type: "USE_LIMITS" }
Nested (Map of Lists)
.customType("solver_groups", "Map[String,List[BaseSolver]]")
.acceptsDerivedTypes()
solver_groups:
planning:
- class: TrajOptSolver
config: { max_iterations: 100 }
- class: OMPLSolver
config: { planner_id: "RRTstar" }
Architecture
Key Components
- validatePluginInfo() - New validator function
- Validates individual plugin info structures (class + config)
- Checks "class" field identifies a valid derived type
- Validates "config" against the derived type's schema
- Provides clear, hierarchical error messages
- validateCustomType() - Enhanced existing function
- Detects
acceptsDerivedTypes() attribute
- Uses plugin info structure validation when enabled
- Supports single entries, sequences, and maps
- Maintains backward compatibility
Usage Guide
Declaring Fields for Derived Types
To enable plugin info structure validation on a field, use acceptsDerivedTypes():
PropertyTreeBuilder()
.customType("constraint", "BaseConstraint")
.acceptsDerivedTypes()
.done()
Single Entry Example
For a single derived type instance:
- Schema
auto schema = PropertyTreeBuilder()
.container("planning_config")
.customType("constraint", "BaseConstraint")
.acceptsDerivedTypes()
.doc("A single constraint instance")
.done()
.done()
.build();
- YAML
planning_config:
constraint:
class: JointPositionConstraint
config:
joint: "shoulder_1"
position: 0.785
tolerance: 0.01
Array/Vector Example
For multiple instances of derived types:
- Schema
auto schema = PropertyTreeBuilder()
.container("planning_problem")
.customType("constraints", "List[BaseConstraint]")
.acceptsDerivedTypes()
.doc("Array of constraints (each can be any derived type)")
.done()
.done()
.build();
- YAML
planning_problem:
constraints:
- class: JointPositionConstraint
config:
joint: "joint_1"
position: 0.0
tolerance: 0.005
- class: CartesianVelocityConstraint
config:
frame: "tool0"
max_velocity: 1.5
- class: CollisionConstraint
config:
constraint_type: "USE_LIMITS"
safety_margin: 0.05
Map Example
For named instances of derived types:
- Schema
auto schema = PropertyTreeBuilder()
.container("scenario")
.customType("constraints", "Map[String,BaseConstraint]")
.acceptsDerivedTypes()
.doc("Map of named constraints")
.done()
.done()
.build();
- YAML
scenario:
constraints:
home_position:
class: JointPositionConstraint
config:
joint: "wrist_3"
position: 1.57
safety_limits:
class: CollisionConstraint
config:
constraint_type: "USE_LIMITS"
safety_margin: 0.05
smooth_motion:
class: CartesianVelocityConstraint
config:
frame: "workspace_center"
max_velocity: 2.0
Registration
Before using derived types, register the type relationships:
PropertyTreeBuilder()
.container("base_constraint")
.doubleNum("weight").defaultVal(1.0).done()
.done()
.build()
);
PropertyTreeBuilder()
.container("joint_position_constraint")
.doubleNum("weight").defaultVal(1.0).done()
.string("joint").required().doc("Joint name").done()
.doubleNum("position").required().doc("Target position").done()
.doubleNum("tolerance").defaultVal(0.01).doc("Position tolerance").done()
.done()
.build()
);
PropertyTreeBuilder()
.container("collision_constraint")
.doubleNum("weight").defaultVal(1.0).done()
.string("constraint_type").required().enumValues({"USE_LIMITS", "PENALTY"}).done()
.doubleNum("safety_margin").defaultVal(0.05).done()
.done()
.build()
);
Validation Process
When validating a property with acceptsDerivedTypes() enabled:
- Extract class field: Required "class" field must be present
- Verify inheritance: Check if the class name is registered as a valid derived type
- Validate config: Validate the "config" field against that type's schema
- Collect errors: All validation errors are collected with proper path context
Error Messages
Error messages are hierarchical and contextual:
"planning_config.constraint: plugin info structure missing required 'class' field"
"planning_config.constraint.class: type 'UnknownConstraint' does not derive from 'BaseConstraint'"
"planning_problem.constraints[1].config.max_velocity: value 1.2 is less than minimum 1.5"
"scenario.constraints[safety_limits].config.constraint_type: not in enum values"
Supported Container Types
Derived types work seamlessly with all PropertyTree container types:
| Container Type | Type Notation | YAML Pattern |
| Single Entry | BaseType | { class: ..., config: ... } |
| Dynamic Array | List[BaseType] | [ { class: ..., config: ... }, ... ] |
| Fixed Array | List[BaseType,N] | Fixed-size array of plugin infos |
| Map | Map[String,BaseType] | { key1: { class: ..., config: ... }, ... } |
All container combinations support derived types when acceptsDerivedTypes() is set.
Advanced: Nested Containers
Containers can be nested to create complex structures:
- Schema
auto schema = PropertyTreeBuilder()
.container("solver_config")
.customType("solver_groups", "Map[String,List[BaseSolver]]")
.acceptsDerivedTypes()
.doc("Map of solver group names to solver lists")
.done()
.done()
.build();
- YAML
solver_config:
solver_groups:
planning_solvers:
- class: TrajOptSolver
config:
max_iterations: 100
- class: OMPLSolver
config:
planner_id: "RRTstar"
control_solvers:
- class: MoveItControlSolver
config:
group: "manipulator"
Backward Compatibility
The implementation maintains full backward compatibility with the legacy "type" field approach:
- Legacy Approach (still works)
constraint:
type: JointPositionConstraint
joint: "joint_1"
position: 0.785
This works when acceptsDerivedTypes() is not set. However, the plugin info structure is now the recommended approach because it:
- Clearly separates type identification from configuration
- Aligns with Tesseract's standard PluginInfo pattern
- Supports better composition and nesting
- Provides clearer YAML organization
Derived Types vs. OneOf
Both features enable polymorphic-like configuration but serve different purposes:
| Feature | Derived Types | OneOf |
| Use Case | Type inheritance hierarchies (heterogeneous) | Mutually exclusive variants |
| Type Specification | Explicit "class" field | Implicit - determined by required fields present |
| Flexibility | Each element can be different derived type | All choices defined at schema time |
| Extensibility | Very easy - register new types at runtime | Requires schema modification |
| Structure | Plugin info (class + config) | Multiple sibling containers |
| Array/Map Elements | Can mix different types | Must be single type variant |
| Example Use Case | Constraints (different at each position) | Shape types (Circle vs. Rectangle) |
When to use Derived Types
- Type hierarchies with inheritance relationships
- Plugin architectures where new types are registered dynamically
- Collections that mix different derived types in same array/map
- Standard type selection via "class" field (plugin info pattern)
When to use OneOf
- Mutually exclusive configuration options
- Variant types without inheritance notion
- Configuration structure determines type
Complete Working Example
See the comprehensive example at:
- Doxygen Source File:
common/examples/property_tree_derived_types_example.cpp
The example demonstrates:
- Single constraint entry
- Array of mixed constraint types
- Named constraints in a map
- Error handling and validation
- Registration patterns
Implementation Details
The implementation is located in:
Modified Files
New Function: validatePluginInfo()
- Located:
property_tree.cpp (lines 1066-1154)
- Validates: Plugin info structure with class + config
- Handles: Single entries, sequences, and maps
Enhanced Function: validateCustomType()
- Located:
property_tree.cpp (lines 955-1065)
- Detects:
acceptsDerivedTypes attribute
- Routes: To plugin info validation when enabled
- Supports: All container types (single, sequence, map)
Best Practices
- Use Plugin Info Structure: Always use class + config pattern with
acceptsDerivedTypes()
- Register Early: Register derived types at module initialization
- Clear Class Names: Use fully qualified type names (e.g., "tesseract::planning::JointPositionConstraint")
- Validate Config: Always include required fields in derived type schemas
- Error Handling: Check validation results before accessing configuration
- Documentation: Add description metadata to explain which types are valid
- Consistency: Use nested config for all type-specific parameters
- Testing: Test with all registered derived types
- Versioning: Consider schema version in derived type registration for compatibility
Quick Reference Guide
Plugin Info Structure
Every derived type instance has two fields:
field:
class: TypeIdentifier # Type name (required)
config: # Type-specific parameters (optional)
param1: value1
param2: value2
Common Error Messages
| Error | Meaning | Solution |
| plugin info structure missing required class field | No class identifier | Add class: TypeName |
| type X does not derive from BaseType | Invalid type name | Register the type or use a valid derived type |
| value cannot be empty (in config) | Config validation failed | Check schema requirements for the type |
| expected a plugin info structure | Wrong YAML structure | Use { class: ..., config: ... } format |
Validation Flow
- Check "class" field exists (required)
- Verify class is registered as derived from base type
- Validate "config" against that type's schema
- Collect all errors with path context
Schema Attributes
Key attributes for derived type fields:
.customType("field_name", "BaseType")
.acceptsDerivedTypes()
.doc("Description")
.required()
.done()
The .acceptsDerivedTypes() attribute is what enables plugin info structure validation.
Container Types
| Container | Type Notation | YAML Example |
| Single | BaseType | { class: Type, config: {...} } |
| Array | List[BaseType] | [ { class: Type, config: {...} }, ... ] |
| Fixed Array | List[BaseType,N] | Fixed-size array of plugin infos |
| Map | Map[String,BaseType] | { key1: { class: Type, config: {...} }, ... } |
Implementation Patterns
Pattern 1: Extensible Plugin System
.customType("solver", "BaseSolver")
.acceptsDerivedTypes()
.doc("Any registered solver implementation can be used here")
Pattern 2: Multiple Configurations
.customType("constraints", "List[BaseConstraint]")
.acceptsDerivedTypes()
.doc("List of constraints - can mix different types")
Pattern 3: Named Component Map
.customType("plugins", "Map[String,BasePlugin]")
.acceptsDerivedTypes()
.doc("Named plugin instances")
Legacy "Type" Field
When acceptsDerivedTypes() is not set, the old "type" field approach still works:
constraint:
type: JointPositionConstraint
joint: "joint_1"
position: 0.785
However, use plugin info structure (class + config) for new code.
Migration Guide
To migrate from the legacy "type" field approach:
- Before (legacy, still works)
constraints:
- type: JointPositionConstraint
joint: "joint_1"
position: 0.0
.customType("constraints", "List[BaseConstraint]")
- After (recommended)
constraints:
- class: JointPositionConstraint
config:
joint: "joint_1"
position: 0.0
.customType("constraints", "List[BaseConstraint]")
.acceptsDerivedTypes()
- Changes Required
- Add
.acceptsDerivedTypes() to the schema definition
- Restructure YAML: wrap config in "config" field
- Change "type" field to "class" field in YAML files
- No C++ code changes to validation logic - automatic detection of new structure