Field Value Validation
Field value validation in FlatMessage provides a powerful mechanism for handling deserialization failures gracefully, making it particularly useful for versioning scenarios. The validate
attribute allows you to specify how the system should behave when a field cannot be deserialized successfully.
Validation Modes
FlatMessage supports two validation modes:
validate = strict
(default): Deserialization fails immediately if any field cannot be deserializedvalidate = fallback
: Falls back to the field's default value if deserialization fails
Syntax
The validate
attribute can be applied at two levels:
Structure Level
#![allow(unused)] fn main() { #[derive(FlatMessage)] #[flat_message_options(validate = fallback)] struct MyStruct { // All fields will use fallback validation by default } }
Field Level
#![allow(unused)] fn main() { #[derive(FlatMessage)] struct MyStruct { #[flat_message_item(validate = strict)] critical_field: u32, #[flat_message_item(validate = fallback)] optional_field: Color, } }
Remarks:: The structure level validate
attribute can be overridden at the field level (by useing #[flat_message_item(validate = "...")]).
Common Use Cases
1. Enum Evolution with Backward Compatibility
When adding new variants to enums, validate = fallback
allows older code to handle newer enum values gracefully:
#![allow(unused)] fn main() { // Version 1 #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug, Default)] #[repr(u8)] pub enum Color { #[default] Red = 1, Green = 10, Blue = 100, } #[derive(Debug, PartialEq, Eq, FlatMessage)] #[flat_message_options(store_name = false)] pub struct TestStruct { pub value: u8, #[flat_message_item(repr = u8, kind = enum, validate = fallback)] pub color: Color, } }
#![allow(unused)] fn main() { // Version 2 - adds Yellow variant #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)] #[repr(u8)] pub enum Color { Red = 1, Green = 10, Blue = 100, Yellow = 200, // New variant } #[derive(Debug, PartialEq, Eq, FlatMessage)] #[flat_message_options(store_name = false)] pub struct TestStruct { pub value: u8, #[flat_message_item(repr = u8, kind = enum)] pub color: Color, } }
When v1 code tries to deserialize data containing Yellow
(value 200), it will fallback to the default Red
value instead of failing:
#![allow(unused)] fn main() { // v2 serializes data with Yellow let d_v2 = v2::TestStruct { value: 1, color: v2::Color::Yellow }; d_v2.serialize_to(&mut storage, Config::default()).unwrap(); // v1 deserializes successfully with fallback to default let d_v1 = v1::TestStruct::deserialize_from(&storage).unwrap(); assert_eq!(d_v1.value, 1); assert_eq!(d_v1.color, v1::Color::Red); // Fallback to default }
2. Flags Evolution with Backward Compatibility
Similar to enums, flags can evolve by adding new flag variants. Using validate = fallback
allows older code to handle newer flag combinations gracefully:
#![allow(unused)] fn main() { // Version 1 #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug, Default)] #[repr(transparent)] #[flags(A,B)] pub struct Flags(u8); impl Flags { add_flag!(A = 1); add_flag!(B = 2); } #[derive(Debug, PartialEq, Eq, FlatMessage)] #[flat_message_options(store_name = false)] pub struct TestStruct { pub value: u8, #[flat_message_item(repr = u8, kind = flags, validate = fallback)] pub flags: Flags, } }
#![allow(unused)] fn main() { // Version 2 - adds C flag #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug, Default)] #[repr(transparent)] #[flags(A,B,C)] pub struct Flags(u8); impl Flags { add_flag!(A = 1); add_flag!(B = 2); add_flag!(C = 4); // New flag } #[derive(Debug, PartialEq, Eq, FlatMessage)] #[flat_message_options(store_name = false)] pub struct TestStruct { pub value: u8, #[flat_message_item(repr = u8, kind = flags)] pub flags: Flags, } }
When v1 code tries to deserialize data containing the new C
flag, it will fallback to the default empty flags instead of failing:
#![allow(unused)] fn main() { // v2 serializes data with C flag (unknown to v1) let d_v2 = v2::TestStruct { value: 1, flags: v2::Flags::C | v2::Flags::B }; d_v2.serialize_to(&mut storage, Config::default()).unwrap(); // v1 deserializes successfully with fallback to default let d_v1 = v1::TestStruct::deserialize_from(&storage).unwrap(); assert_eq!(d_v1.value, 1); assert!(d_v1.flags.is_empty()); // Fallback to default (empty flags) }
3. Variant Evolution with Backward Compatibility
Variants (also known as tagged unions or sum types) can evolve by adding new variants. Using validate = fallback
allows older code to handle newer variant types gracefully by falling back to a default variant:
#![allow(unused)] fn main() { // Version 1 #[derive(FlatMessageVariant, Debug, PartialEq, Eq)] pub enum DataVariant { Byte(u8), String(String), } impl Default for DataVariant { fn default() -> Self { DataVariant::Byte(0) } } #[derive(Debug, PartialEq, Eq, FlatMessage)] #[flat_message_options(store_name = false)] pub struct TestStruct { pub value: u8, #[flat_message_item(kind = variant, align = 1, validate = fallback)] pub data: DataVariant, } }
#![allow(unused)] fn main() { // Version 2 - adds DWord variant #[derive(FlatMessageVariant, Debug, PartialEq, Eq)] pub enum DataVariant { Byte(u8), String(String), DWord(u32), // New variant } #[derive(Debug, PartialEq, Eq, FlatMessage)] #[flat_message_options(store_name = false)] pub struct TestStruct { pub value: u8, #[flat_message_item(kind = variant, align = 1)] pub data: DataVariant, } }
When v1 code tries to deserialize data containing the new DWord
variant, it will fallback to the default variant instead of failing:
#![allow(unused)] fn main() { // v2 serializes data with DWord variant (unknown to v1) let d_v2 = v2::TestStruct { value: 1, data: v2::DataVariant::DWord(12345) }; d_v2.serialize_to(&mut storage, Config::default()).unwrap(); // v1 deserializes successfully with fallback to default let d_v1 = v1::TestStruct::deserialize_from(&storage).unwrap(); assert_eq!(d_v1.value, 1); assert_eq!(d_v1.data, v1::DataVariant::Byte(0)); // Fallback to default variant }
Important Notes for Variants:
- The variant type must implement the
Default
trait for fallback validation to work - Unlike enums, variants cannot use the
#[default]
attribute on individual variants due to their complex data structure - The
Default
implementation should return a sensible default variant with appropriate default values for its contained data
4. Graceful Schema Evolution
Fallback validation enables smooth transitions when field types change or become incompatible:
#![allow(unused)] fn main() { #[derive(FlatMessage)] #[flat_message_options(validate = fallback)] // Structure-level fallback validation struct Configuration { #[flat_message_item(default = 30)] timeout_seconds: u32, log_level: LogLevel, // Uses structure-level fallback validation #[flat_message_item(validate = strict)] // Override to strict for critical field database_url: String, // Critical field - must not fail } }
How Fallback Validation Works
When validate = fallback
is specified:
- Normal Operation: If the field can be deserialized normally, it uses the stored value
- Fallback Trigger: If deserialization fails (e.g., enum variant not found, type mismatch), the system:
- Uses the
default
value if specified in the attribute - Uses the type's
Default::default()
implementation - For
Option<T>
types, defaults toNone
- Uses the
Best Practices
When to Use Strict Validation
- Critical fields where data integrity is essential
- When you need to detect and handle incompatibilities explicitly
- Fields where a default value doesn't make semantic sense
When to Use Fallback Validation
- Optional or non-critical fields
- Enum/Flags/Variant fields that may evolve over time
- During data migration periods
- When maintaining backward compatibility is important
Combining with Other Attributes
#![allow(unused)] fn main() { #[derive(FlatMessage)] struct FlexibleStruct { // Critical field - must be present and valid #[flat_message_item(mandatory = true, validate = strict)] user_id: u64, // Optional field with fallback and custom default #[flat_message_item( mandatory = false, validate = fallback, default = "guest" )] username: String, // Enum that can evolve safely #[flat_message_item( repr = u8, kind = enum, validate = fallback )] user_type: UserType, } }
Implementation Notes
- The
validate
attribute affects only the deserialization process - Serialization is not affected by validation settings
- Fallback validation requires the type to implement
Default
trait - For enums, the
#[default]
attribute must be specified on one variant - The validation behavior is determined at compile time through proc macros