Tesseract 0.28.4
Loading...
Searching...
No Matches
Derived Type Support in PropertyTree

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

// Register schemas
PropertyTreeBuilder().container("base_constraint").done().build()
);
TESSERACT_SCHEMA_REGISTER(JointPositionConstraint,
PropertyTreeBuilder()
.container("jp_constraint")
.string("joint").required().done()
.doubleNum("position").required().done()
.done()
.build()
);
// Register inheritance relationships
TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE(BaseConstraint, JointPositionConstraint);
#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() // Enable plugin info structure validation
.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

  1. 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
  2. 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() // Enable plugin info validation
.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:

// 1. Register the base type schema
PropertyTreeBuilder()
.container("base_constraint")
.doubleNum("weight").defaultVal(1.0).done()
.done()
.build()
);
// 2. Register derived type schemas
TESSERACT_SCHEMA_REGISTER(JointPositionConstraint,
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()
);
TESSERACT_SCHEMA_REGISTER(CollisionConstraint,
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()
);
// 3. Register the inheritance relationships
TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE(BaseConstraint, JointPositionConstraint);
TESSERACT_SCHEMA_REGISTER_DERIVED_TYPE(BaseConstraint, CollisionConstraint);

Validation Process

When validating a property with acceptsDerivedTypes() enabled:

  1. Extract class field: Required "class" field must be present
  2. Verify inheritance: Check if the class name is registered as a valid derived type
  3. Validate config: Validate the "config" field against that type's schema
  4. Collect errors: All validation errors are collected with proper path context

Error Messages

Error messages are hierarchical and contextual:

// Missing required 'class' field
"planning_config.constraint: plugin info structure missing required 'class' field"
// Invalid derived type
"planning_config.constraint.class: type 'UnknownConstraint' does not derive from 'BaseConstraint'"
// Configuration validation error
"planning_problem.constraints[1].config.max_velocity: value 1.2 is less than minimum 1.5"
// Map value error
"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

  1. Use Plugin Info Structure: Always use class + config pattern with acceptsDerivedTypes()
  2. Register Early: Register derived types at module initialization
  3. Clear Class Names: Use fully qualified type names (e.g., "tesseract::planning::JointPositionConstraint")
  4. Validate Config: Always include required fields in derived type schemas
  5. Error Handling: Check validation results before accessing configuration
  6. Documentation: Add description metadata to explain which types are valid
  7. Consistency: Use nested config for all type-specific parameters
  8. Testing: Test with all registered derived types
  9. 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

  1. Check "class" field exists (required)
  2. Verify class is registered as derived from base type
  3. Validate "config" against that type's schema
  4. Collect all errors with path context

Schema Attributes

Key attributes for derived type fields:

.customType("field_name", "BaseType") // Mark as custom type
.acceptsDerivedTypes() // Enable plugin info validation [REQUIRED]
.doc("Description") // Document the field
.required() // Make the field mandatory
.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]")
// No acceptsDerivedTypes() call
After (recommended)
constraints:
- class: JointPositionConstraint
config:
joint: "joint_1"
position: 0.0
.customType("constraints", "List[BaseConstraint]")
.acceptsDerivedTypes() // Add this line
Changes Required
  1. Add .acceptsDerivedTypes() to the schema definition
  2. Restructure YAML: wrap config in "config" field
  3. Change "type" field to "class" field in YAML files
  4. No C++ code changes to validation logic - automatic detection of new structure