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 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
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:
- Wrap in
schemas: block
- Remove
name: (becomes key in schemas)
- Remove
rust: section (automatic now)
- Convert
fields: from array to object
- Use inline syntax:
type name: instead of - name: ... type: ...
Primitive Types
All protobuf primitive types are supported:
| Type | Rust | Size | Description |
|---|
bool | bool | 1 | Boolean |
int8 | i8 | 1 | Signed 8-bit integer |
int16 | i16 | 2 | Signed 16-bit integer |
int32 | i32 | 4 | Signed 32-bit integer |
int64 | i64 | 8 | Signed 64-bit integer |
uint8 | u8 | 1 | Unsigned 8-bit integer |
uint16 | u16 | 2 | Unsigned 16-bit integer |
uint32 | u32 | 4 | Unsigned 32-bit integer |
uint64 | u64 | 8 | Unsigned 64-bit integer |
float | f32 | 4 | 32-bit floating point |
double | f64 | 8 | 64-bit floating point |
string_fixed[N] | [u8; N] | N | Fixed-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
| Feature | Legacy Format | V2 Format |
|---|
| Field definition | 3-5 lines | 1-2 lines |
| Index groups | Duplicated | Shared, 0 duplication |
| Multiple schemas | Multiple files | Single file |
| Nested types | Not supported | Fully supported |
| Imports | Not supported | Fully supported |
| Language config | Required | Automatic |
| Readability | Verbose | Concise |
Best Practices
# ✅ 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