struct_data
A pure Dart library for structured data using field descriptors. Lightweight serialization without code generation. Bit-level, byte-level, and word-level typed structs inspired by C-style structs and unions. Enum-keyed maps and binary codecs for embedded protocols and data serialization.
Features
Binary Structs
BitStruct— Pack and extract arbitrary bit-width fields within a single integer usingBitmaskdescriptors. Ideal for hardware registers, flags, and compact binary protocols.WordStruct— Byte-aligned fields within a single 64-bit integer. Useful for compact multi-field values like version numbers and calibration parameters.ByteStruct— Keyed access to typed fields withinTypedDatabuffers viaByteFielddescriptors. SupportsInt8,Uint16,Int32, etc. for packet payloads and binary frames.BinaryFormat/BinaryCodec— Encode and decode between typed values (int,double,bool,Enum) and raw binary integers. Includes fixed-point formats (Fract16,Accum16, etc.).BitsMap/BoolMap— Efficient flag and boolean collections backed by a single integer.
Structured Data
StructData— Zero-cost extension type providing keyedoperator[]access over any object viaFielddescriptors. No allocation, no wrapper overhead.Serializable— Mixin for declarative JSON serialization using enum field keys. ProvidestoMap(),toJson(), value equality, and immutable copy helpers — without code generation.EnumMap/IndexMap— Type-safe, fixed-key collections backed by parallel arrays. Built-in JSON serialization viaEnum.name.- Utility extensions — Numeric conversions, typed data slicing, null-safe helpers, and string operations.
Getting Started
Add struct_data to your pubspec.yaml:
dependencies:
struct_data: ^0.1.0
Import the full library or only the binary data subset:
import 'package:struct_data/struct_data.dart'; // Full library
import 'package:struct_data/binary_data.dart'; // Binary structs only
Note: This package uses
dart:ffinative types (Uint8,Int16,Int32, etc.) as compile-time type markers for field sizing. No FFI calls are made at runtime. Packet may selectively implement over ffi.Struct.
Usage
Bit-level operations
Use Bits for raw bitwise manipulation and BitStruct for named field access:
// Raw bit operations
const flags = Bits(0xFF);
print(flags.boolAt(0)); // true
print(flags.bitsAt(4, 4)); // 15 (upper nibble)
// Named bit fields via enum
enum StatusField with BitField {
ready(Bitmask(0, 1)), // bit 0, width 1
error(Bitmask(1, 1)), // bit 1, width 1
mode(Bitmask(2, 3)); // bits 2-4, width 3
const StatusField(this.bitmask);
@override
final Bitmask bitmask;
}
final status = BitStruct<StatusField>.from(0x05);
print(status[StatusField.ready]); // 1
print(status[StatusField.mode]); // 1
// Immutable field update
final errorStatus = status.withField(StatusField.error, 1);
Word-level structs
Byte-aligned fields packed in a single integer, suitable for version numbers, calibration parameters, and compact identifiers:
// Built-in Version model — 4 byte-sized fields in a single int
const version = VersionStandard(1, 2, 3, 0, name: 'App');
print(version.toStringAsVersion()); // 1.2.3.0
// Immutable copy with modified field
final patched = version.withField(VersionFieldStandard.fix, 1);
print(patched.toStringAsVersion()); // 1.2.3.1
// Custom WordStruct with mixed field sizes
enum SensorField<V extends NativeType> with WordField<V>, TypedField<V> {
deviceId<Uint16>(0),
sensorType<Uint8>(2),
flags<Uint8>(3),
reading<Int32>(4);
const SensorField(this.offset);
@override
final int offset;
}
final sensor = const WordStruct<SensorField>(Word.of32s(0x002AA005, 0x00001234));
print(sensor[SensorField.deviceId]); // 0x1234
print(sensor[SensorField.reading]); // 42
Byte-level structs
Keyed access to typed fields within ByteData buffers for packet payloads and binary protocols:
enum TelemetryField<V extends NativeType> with ByteField<V>, TypedField<V> {
timestamp<Uint32>(0),
deviceId<Uint16>(4),
status<Uint8>(6),
value<Int32>(8);
const TelemetryField(this.offset);
@override
final int offset;
}
final buffer = ByteData(12);
final frame = ByteStruct<TelemetryField>(buffer);
frame[TelemetryField.timestamp] = 1700000000;
frame[TelemetryField.deviceId] = 0x1234;
frame[TelemetryField.value] = -42;
print(frame[TelemetryField.value]); // -42
Serializable mixin
Declarative JSON serialization without code generation. Define a field enum with SerializableField, then mix Serializable into the data class:
enum PersonField<V extends Object> with SerializableField<V> {
id<int>(),
name<String>(),
age<int>();
@override
V getIn(covariant Person struct) => switch (this) {
PersonField.id => struct.id as V,
PersonField.name => struct.name as V,
PersonField.age => struct.age as V,
};
@override
void setIn(covariant Person struct, V value) => throw UnsupportedError('immutable');
@override
bool testAccess(Object struct) => struct is Person;
}
class Person with Immutable<Person>, Serializable<Person> {
const Person(this.id, this.name, this.age);
Person.fromMap(Map<SerializableField, Object?> map)
: id = map[PersonField.id] as int,
name = map[PersonField.name] as String,
age = map[PersonField.age] as int;
factory Person.fromJson(Map<String, Object?> json) =>
Person.fromMap(const StructForm(PersonField.values).fromJson(json));
final int id;
final String name;
final int age;
@override
List<PersonField> get keys => PersonField.values;
@override
Person copyWithMap(covariant Map<SerializableField, Object?> data) => Person.fromMap(data);
}
final person = Person.fromJson({'id': 1, 'name': 'Alice', 'age': 30});
print(person.toJson()); // {id: 1, name: Alice, age: 30}
print(person.withField(PersonField.age, 31).toJson()); // {id: 1, name: Alice, age: 31}
print(person == Person(1, 'Alice', 30)); // true (value equality)
Binary format codecs
Encode and decode between typed values and raw binary integers:
const fract = Fract16(); // Q1.15 fixed-point
print(fract.decode(16384)); // 0.5
print(fract.encode(0.5)); // 16384
const boolFmt = BoolFormat();
print(boolFmt.decode(1)); // true
print(boolFmt.encode(false)); // 0
const int16 = Int16Int();
print(int16.decode(0xFFFF)); // -1 (sign-extended)
Enum-keyed maps
Type-safe, fixed-key collections with built-in JSON serialization:
enum Color { red, green, blue }
final colors = IndexMap.of(Color.values, [0xFF0000, 0x00FF00, 0x0000FF]);
print(colors[Color.red]); // 16711680
print(colors.toJson()); // {red: 16711680, green: 65280, blue: 255}
Architecture
Design Principles
- Zero-cost abstractions —
BitStruct,WordStruct,ByteStruct,StructData, andWordare Dart extension types. They provide typed, keyed access with no runtime allocation or wrapper overhead — the compiler erases them entirely. - Field-as-descriptor — Accessor logic lives on the key (
Field.getIn/Field.setIn), not the struct. This keeps data classes plain and enables the same field schema to work acrossStructDataviews,StructBasesubtypes, and serialization. - Enum-driven schemas — Field enums serve as both the schema definition and the serialization key.
Enum.nameprovides JSON keys for free viaEnumMapByName. - No code generation — All serialization, field dispatch, and binary encoding is defined in plain Dart. No
build_runner, no generated files, no build step. - Immutability-first — Binary structs use functional
withField/withMapcopies.Serializableclasses opt into immutable copies via theImmutablemixin. - Compile-time const —
BitStruct,WordStruct,Word, andBitsvalues can beconst, enabling use as enum entries and compile-time constants.
Struct Hierarchy
Three tiers of structured binary data, each backed by progressively larger storage:
| Type | Backing Storage | Granularity | Typical Size | Use Case |
|---|---|---|---|---|
BitStruct<K> |
int |
Individual bit ranges | 1–64 bits | Hardware registers, flags, compact protocols |
WordStruct<K> |
int (via Word) |
Byte-aligned ranges | 1–8 bytes | Version numbers, calibration, identifiers |
ByteStruct<K> |
ByteData |
Typed byte offsets | Arbitrary | Packet payloads, telemetry frames |
All three are extension types wrapping their backing storage. Field access is dispatched through enum keys implementing BitField, WordField, or ByteField respectively.
Core Abstractions
| Type | Role |
|---|---|
StructData<K, V> |
Zero-cost keyed view over any object via Field keys |
Field<V> |
Interface for field descriptors: getIn, setIn, testAccess |
StructForm<K, V> |
Schema definition (wraps List<K>); bridges to Map and serialization |
StructBase<S, K, V> |
Mixin for user-defined struct classes holding data in their own fields |
Serializable<S> |
Mixin providing toMap(), toJson(), value equality via SerializableField keys |
Immutable<S> |
Mixin providing withField, withFields, withMap for functional copies |
Collections
| Type | Key Type | Use Case |
|---|---|---|
EnumMap<E, V> |
Enum |
Type-safe enum-keyed map with JSON via Enum.name |
IndexMap<K, V> |
Enum (by .index) |
Dense, fixed-size collection backed by parallel arrays |
BitsMap<K> / BoolMap<K> |
BitField enum |
Bit-flag and boolean collections backed by a single integer |
Binary Formats
Codecs for encoding typed values to and from integer storage:
| Format | Dart Type | Description |
|---|---|---|
IntFormat<S> |
int |
Raw integer with optional sign extension |
FractFormat<S> |
double |
Fixed-point fractional (Q-format) |
FixedPoint<S> |
double |
Configurable Q-format fixed-point |
BoolFormat |
bool |
Boolean as 0/1 |
EnumFormat<V> |
Enum |
Enum by index |
BinaryQuantityCodec<V> |
num |
Value with scaling and unit conversion |
Benchmarks
The example/benchmark.dart file provides local benchmarks for BitStruct, WordStruct, ByteStruct, and Serializable, with comparisons against published metrics for Protocol Buffers, json_serializable, freezed, and PackMe.
| Approach | Encode | Decode | Wire Size | Codegen |
|---|---|---|---|---|
BitStruct (8 fields) |
0.013 µs | 0.022 µs | Minimal (bit-packed) | None |
WordStruct (8 bytes) |
0.039 µs | 0.038 µs | Minimal (byte-packed) | None |
ByteStruct (16 bytes) |
0.073 µs | 0.079 µs | Exact (fixed layout) | None |
Serializable mixin |
0.197 µs | 0.150 µs | JSON | None |
| json_serializable | ~0.3–0.5 µs | ~0.4–0.6 µs | JSON | build_runner |
| freezed | ~0.3–0.5 µs | ~0.4–0.6 µs | JSON | build_runner |
| Protocol Buffers | ~0.5–2.0 µs | ~0.5–2.0 µs | Compact (varint) | protoc |
| PackMe | ~1.0–3.0 µs | ~1.0–3.0 µs | Compact (tagged) | packme CLI |
Run with dart run example/benchmark.dart.
Additional Information
- Minimum SDK: Dart 3.10.0
- Dependencies:
collection,meta - License: MIT
- Repository: github.com/FireSourcery/cmdr
- Issues: github.com/FireSourcery/cmdr/issues
Libraries
- binary_data
- Bits and byte structures, and binary format/codec for encoding/decoding to/from binary data.
- binary_format/binary_codec
- binary_format/binary_format
- binary_format/quantity_format
- bits/bit_field
- bits/bit_struct
- bits/bits
- bits/bits_map
- bytes/byte_struct
- bytes/typed_array
- bytes/typed_data_buffer
- bytes/typed_data_ext
- bytes/typed_field
- general/enum_map
- general/enum_types
- general/index_map
- general/serializable
- general/struct
- models/version
- packet/packet
- packet/packet_ext
- packet/packet_header
- packet/packet_id
- packet/packet_transformer
- struct_data
- Bitwise operations, binary data manipulation, typed structs, and serialization utilities for Dart.
- utilities/basic_ext
- utilities/basic_types
- utilities/num_ext
- utilities/slice
- word/word
- word/word_struct