This project demonstrates how to create a seamless integration between Rust and Python, combining the performance benefits of Rust with Python's rich data validation ecosystem. Specifically, it shows how to expose Rust structs to Python via PyO3 and make them fully compatible with Pydantic models and ormsgpack serialization.
- 🦀 Rust data structures exposed to Python via PyO3
- 🔍 Pydantic validation and type checking with Rust struct references
- 📦 Efficient serialization with ormsgpack
- 🔄 Complete round-trip serialization (Rust → Python → bytes → Python → Rust)
This example demonstrates a powerful pattern for Python/Rust interoperability:
-
Rust Side (PyO3):
- Defines a
Personstruct with attributes and methods - Implements a
Wrappertrait providing dictionary conversion functionality - Exposes the class and utility functions to Python
- Defines a
-
Python Side (Pydantic):
- Creates a
PersonWrapperannotated type that combines:- The Rust
Personclass as the base type PlainValidatorusing thePerson.validatemethodPlainSerializerusing the__dict__method
- The Rust
- Wraps the type in Pydantic models for validation
- Creates a
-
Serialization Flow:
- Rust objects are created and passed to Python
- Pydantic models validate and wrap these objects
- ormsgpack serializes to binary format
- Deserialization reconstructs the complete object hierarchy
- The nested Person struct is fully preserved, and equal to the original struct
# Clone the repository
git clone git@github.com:LockedThread/py03_pydantic_ormsgpack_experiment.git
cd py03_pydantic_ormsgpack_experiment
# Install Python dependencies
pip install pydantic ormsgpack
cargo runfrom pydantic import BaseModel, PlainSerializer, PlainValidator
from typing import Annotated
import ormsgpack
from py03_pydantic_ormsgpack_experiment import Person, create_nested_person
# Create a type annotation for Person with serialization capabilities
PersonWrapper = Annotated[
Person,
PlainValidator(Person.validate),
PlainSerializer(lambda v: v.__dict__, return_type=dict),
]
# Use the wrapper in a Pydantic model
class UserModel(BaseModel):
person: PersonWrapper
# Create a Person instance from Rust
person = create_nested_person(depth=2, max_children=3)
# Validate using Pydantic
user = UserModel(person=person)
# Serialize to bytes with ormsgpack
serialized = ormsgpack.packb(user.model_dump())
# Deserialize back to a valid model
deserialized = ormsgpack.unpackb(serialized)
restored_user = UserModel(**deserialized)
# The nested Person structure is fully preserved!
assert isinstance(restored_user.person, Person)
assert isinstance(restored_user.person.children[0], Person)The key to making this work is the Wrapper trait in Rust:
pub trait Wrapper {
fn to_dict_with_py<'a>(&'a self, py: Python<'a>) -> PyResult<Bound<'a, PyDict>>;
fn to_dict(&self) -> PyResult<Py<PyDict>>;
fn from_dict(dict: &Bound<'_, PyDict>) -> PyResult<Self> where Self: Sized;
fn validate(value: &Bound<'_, PyAny>) -> PyResult<Self> where Self: Sized;
}This trait enables bidirectional conversion between Rust structs and Python dictionaries, which is then exposed to Python via PyO3's #[pymethods].
On the Python side, we use Pydantic's Annotated types with:
PlainValidator- Converts from Python objects to the Rust typePlainSerializer- Converts from the Rust type to Python dictionaries
This enables seamless integration with Pydantic's validation system while preserving the type information.
This pattern is particularly useful for:
- High-performance data processing pipelines
- Applications needing both speed and type safety
- APIs that handle complex nested data structures
- Projects transitioning from Python to Rust incrementally
- Type Safety: Ensure data consistency with Pydantic's validation
- Serialization: Efficiently transmit data with ormsgpack's compact format
- Developer Experience: Maintain Python's ease of use while getting Rust's benefits
MIT
Contributions are welcome! Please feel free to submit a Pull Request.