Skip to main content

Schema Format

Cerulion Core supports two schema formats: V2 format (recommended) and legacy format (backward compatible). This page provides a complete reference for both. V2 format is a modern, ROS2-inspired schema language that simplifies message type definitions. It features inline field syntax, multi-schema files, shared index groups, and imports.

Quick Start

Here’s a simple V2 schema:
schemas:
  SensorData:
    description: Temperature and pressure sensor readings
    fields:
      float temperature:
        description: Temperature in Celsius
      float pressure:
        description: Pressure in hPa
      uint64 timestamp:
        description: Timestamp in nanoseconds
Generated Rust:
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct SensorData {
    pub temperature: f32,
    pub pressure: f32,
    pub timestamp: u64,
}

Field Syntax

Format: type[size] name:
fields:
  # Simple types
  float temperature:
  uint64 timestamp:
  bool is_active:
  
  # Fixed-size arrays
  float[3] position:
  uint8[16] uuid:
  
  # Fixed-size strings (Copy-compatible)
  string_fixed[32] username:
  
  # Custom types
  Position3D location:
  
  # Arrays of custom types
  Position3D[4] waypoints:
The inline syntax float[20] positions: is equivalent to the legacy format’s separate name, type, and size fields, but much more concise.

Multi-Schema Files

Define multiple related types in one file:
schemas:
  Position3D:
    description: 3D position vector
    fields:
      float x:
      float y:
      float z:
  
  Quaternion:
    description: Rotation quaternion
    fields:
      float w:
      float x:
      float y:
      float z:
  
  Pose:
    description: 6DOF pose
    fields:
      Position3D position:
      Quaternion orientation:
Benefits:
  • Keep related types together
  • Reduce file switching
  • Clear dependencies
  • Single import for related types

Shared Index Groups

Define named array indexes once, reuse across multiple fields:
index_groups:
  joint_names:
    shoulder_left: 0
    elbow_left: 1
    wrist_left: 2
    # ... more joints
    knee_left: 11
    # Indexes 12-19 unnamed but accessible

schemas:
  JointState:
    fields:
      float[20] positions:
        description: Joint positions
        indexes: joint_names  # Reference shared group
      
      float[20] velocities:
        description: Joint velocities
        indexes: joint_names  # Reuse same names!
      
      float[20] effort:
        description: Joint torque
        indexes: joint_names  # No duplication!
Generated code includes named accessors for all fields:
// All three arrays have the same named accessors
state.positions.shoulder_left();
state.positions.set_elbow_left(1.5);

state.velocities.shoulder_left();
state.velocities.set_elbow_left(0.5);

state.effort.shoulder_left();
// ... etc
Shared index groups eliminate duplication. Define joint names once, reuse across positions, velocities, and effort arrays. This makes schemas easier to maintain and ensures consistency.

Imports

Share common definitions across schema files: File: common_types.yaml
schemas:
  Position3D:
    description: 3D position vector
    fields:
      float x:
      float y:
      float z:
File: common_index_groups.yaml
index_groups:
  joint_names:
    shoulder_left: 0
    elbow_left: 1
    # ... more
File: robot_state.yaml
imports:
  - common_types.yaml
  - common_index_groups.yaml

schemas:
  RobotState:
    fields:
      Position3D base_position:  # Imported type
      float[20] joints:
        indexes: joint_names      # Imported index group
Imports allow you to build a library of reusable types and index groups. This promotes consistency across your codebase and reduces duplication.

Custom Nested Types

Compose complex types from simpler ones:
schemas:
  # Base types
  Position3D:
    fields:
      float x:
      float y:
      float z:
  
  Quaternion:
    fields:
      float w:
      float x:
      float y:
      float z:
  
  # Composite type using base types
  Pose:
    fields:
      Position3D position:
      Quaternion orientation:
  
  # Complex type with arrays of custom types
  RobotState:
    fields:
      Pose base_pose:
      Position3D[4] feet_positions:
      uint64 timestamp:
Generated code handles dependencies automatically:
// Position3D generated first
pub struct Position3D { ... }

// Quaternion generated second
pub struct Quaternion { ... }

// Pose generated third (uses Position3D and Quaternion)
pub struct Pose {
    pub position: Position3D,
    pub orientation: Quaternion,
}

// RobotState generated last (uses Pose and Position3D)
pub struct RobotState {
    pub base_pose: Pose,
    pub feet_positions: [Position3D; 4],
    pub timestamp: u64,
}

Legacy Format (Backward Compatible)

Legacy format is still supported for backward compatibility:
name: SensorData
rust:
  derives: [Clone, Copy, Debug]
  repr: C

fields:
  - name: temperature
    type: float
    description: Temperature in Celsius
  - name: pressure
    type: float
    description: Pressure in hPa
  - name: timestamp
    type: uint64
    description: Timestamp in nanoseconds

Format Detection

The parser automatically detects format version:
  • V2 format: Has schemas: key at root level
  • Legacy format: Has name: and fields: at root level
You can mix V2 and legacy format files in the same project. The parser handles both automatically.

Migration Guide

Old format:
name: SensorData
rust:
  derives: [Clone, Copy, Debug]
  repr: C
fields:
  - name: temperature
    type: float
    description: Temperature in Celsius
  - name: timestamp
    type: uint64
New format:
schemas:
  SensorData:
    description: Sensor data
    fields:
      float temperature:
        description: Temperature in Celsius
      uint64 timestamp:
Changes:
  1. Wrap in schemas: block
  2. Remove name: (becomes key in schemas)
  3. Remove rust: section (automatic now)
  4. Convert fields: from array to object
  5. Use inline syntax: type name: instead of - name: ... type: ...

Primitive Types

All protobuf primitive types are supported:
TypeRustSizeDescription
boolbool1Boolean
int8i81Signed 8-bit integer
int16i162Signed 16-bit integer
int32i324Signed 32-bit integer
int64i648Signed 64-bit integer
uint8u81Unsigned 8-bit integer
uint16u162Unsigned 16-bit integer
uint32u324Unsigned 32-bit integer
uint64u648Unsigned 64-bit integer
floatf32432-bit floating point
doublef64864-bit floating point
string_fixed[N][u8; N]NFixed-size byte array (Copy)
Custom types (types not in the above list) are treated as custom types and must be defined in the same file or imported.

Complete Example

Here’s a comprehensive example showing all V2 features:
# Comprehensive example showing all V2 features

imports:
  - common_index_groups.yaml

index_groups:
  rgb_channels:
    red: 0
    green: 1
    blue: 2

schemas:
  Position3D:
    description: 3D position vector in meters
    fields:
      float x:
        description: X coordinate
      float y:
        description: Y coordinate
      float z:
        description: Z coordinate
  
  Color:
    description: RGB color
    fields:
      float[3] channels:
        description: Color channels (0.0 to 1.0)
        indexes: rgb_channels
  
  JointState:
    description: Robot joint state with partial naming
    fields:
      float[20] positions:
        description: Joint positions in radians
        indexes: joint_names  # From common_index_groups.yaml
      float[20] velocities:
        description: Joint velocities in rad/s
        indexes: joint_names
      uint64 timestamp:
        description: Timestamp in nanoseconds
  
  RobotState:
    description: Complete robot state
    fields:
      Position3D base_position:
        description: Base position in world frame
      Position3D[4] feet_positions:
        description: Positions of all 4 feet
      JointState joints:
        description: Current joint state
      Color status_light:
        description: Robot status indicator color
      uint64 timestamp:
        description: State timestamp

Code Generation Defaults

All generated Rust code uses these defaults (no configuration needed):
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct YourStruct {
    // ...
}
Why these defaults?
  • Clone, Copy: Works with cerulion_core zero-copy serialization
  • Debug: Essential for development
  • repr(C): Ensures consistent memory layout across languages
For Python, C++, and other languages, generators will use appropriate defaults for those languages. The schema format is language-agnostic.

Benefits Summary

FeatureLegacy FormatV2 Format
Field definition3-5 lines1-2 lines
Index groupsDuplicatedShared, 0 duplication
Multiple schemasMultiple filesSingle file
Nested typesNot supportedFully supported
ImportsNot supportedFully supported
Language configRequiredAutomatic
ReadabilityVerboseConcise

Best Practices

1. Use V2 Format for New Schemas

# ✅ Good: V2 format (concise, modern)
schemas:
  SensorData:
    fields:
      float temperature:
      uint64 timestamp:

# ⚠️ Works but verbose: Legacy format
name: SensorData
fields:
  - name: temperature
    type: float
# ✅ Good: Related types together
schemas:
  Position3D:
    fields: ...
  Quaternion:
    fields: ...
  Pose:
    fields: ...

# ⚠️ Less organized: Separate files

3. Use Shared Index Groups

# ✅ Good: Define once, reuse
index_groups:
  joint_names:
    shoulder_left: 0
    elbow_left: 1

schemas:
  JointState:
    fields:
      float[20] positions:
        indexes: joint_names
      float[20] velocities:
        indexes: joint_names  # Reuse!

4. Document Your Schemas

# ✅ Good: Documented
schemas:
  SensorData:
    description: Temperature and pressure sensor readings
    fields:
      float temperature:
        description: Temperature in Celsius

Next Steps