firestore_odm 5.0.0
firestore_odm: ^5.0.0 copied to clipboard
Type-safe Firestore ODM with code generation. Exact Firestore semantics: native Timestamp, create/set/patch/delete writes, typed queries, aggregates, transactions, batches and Pipelines.
Firestore ODM π₯ #
Type-safe Firestore ODM for Dart/Flutter - zero reflection, code generation
Zero reflection β’ Full type safety β’ Native Timestamp β’ Measured performance
Documentation β’ Getting Started β’ Examples
π Overview #
Firestore ODM transforms your Firestore development experience with type-safe, intuitive database operations that feel natural and productive.
The Problem:
// Standard cloud_firestore - Runtime errors waiting to happen
DocumentSnapshot doc = await FirebaseFirestore.instance
.collection('users').doc('user123').get();
Map<String, dynamic>? data = doc.data();
String name = data?['name']; // β Runtime error if field doesn't exist
int age = data?['profile']['age']; // β Nested access is fragile
The Solution:
// Firestore ODM - Compile-time safety
User? user = await db.users('user123').get();
String name = user.name; // β
IDE autocomplete, compile-time checking
int age = user.profile.age; // β
Type-safe nested access
Result: Zero reflection, exact Firestore semantics, eliminate runtime errors.
π New in Version 5.0 (clean break, ADR-0002) #
- Exact Firestore semantics - DateTime β native Timestamp both directions; the ODM owns storage serialization.
- Honest write verbs -
create(returns the generated ID),set,patch(six FieldValue-shaped ops),delete. No sentinels, nomodify()magic. - One-shot server-side aggregates - no fake client-side streaming.
- Typed bulk writes -
patchAll/deleteAllchunked to Firestore's 500-write batch limit. - Stable pagination - orderBy with the
$.documentIdtie-breaker selector. - Recorded benchmarks + emulator e2e lane - measured, not asserted.
π New in Version 4.0 #
Building on 3.0's performance foundation, 4.0 expands type coverage and ergonomics on top of a fully reworked code generator.
New in 4.0 #
- β
Enum support -
@JsonValuewith both string and numeric values, enums inorderBy(), and default-value generation - π§ͺ Firestore Pipelines (experimental) -
collection.pipeline()for Enterprise-edition pipeline queries (see the release notes) - β
Automatic nested-class imports - filter, patch, aggregate, and
orderByselectors for nested types need no manual imports - β
Stronger nullable handling - nullable
Mapfields, and nestedfromJsonfactories that accept nullable input no longer crash when a field is missing - β Server timestamps on insert - not only on updates, and honored inside batches
- β
Batch & transaction patch builders - atomic patch operations in
runBatch/runTransaction - β
Reworked code generator - cleaner filter/patch/aggregate/orderBy builders and converters on a unified
FieldPathmodel
Performance (measured, not asserted) #
The repo ships a recorded benchmark harness (apps/flutter_example/test/benchmarks_test.dart,
run by the performance CI lane). Numbers are recorded per run; claims without
measurements are not made.
| Metric | How it is measured |
|---|---|
| Serialization round-trip | BENCH serialization_roundtrip_us_per_op (Stopwatch, 10k iterations) |
| Patch operation latency | BENCH patch_op_ms_per_op (1000 ops against the test double) |
| Runtime overhead | Zero reflection β all magic happens at compile time |
Carried over from 3.0 #
- β
Full generic model support - generic classes with type-safe patch
operations and converter-argument threading (freezed-style
toT/fromT) - β
JsonKey subset (
name,ignore,includeFromJson/includeToJson) and@JsonConverterβ documented honestly, no silent partial support - β
ODM-owned storage serialization - native Timestamp both directions;
your model's
toJson/fromJsonremain for JSON interchange only
β‘ Key Features #
Type Safety Revolution #
| Feature | Standard Firestore | Firestore ODM |
|---|---|---|
| Type Safety | β Map<String, dynamic> | β Strong types throughout |
| Query Building | β String-based, error-prone | β Type-safe with IDE support |
| Data Updates | β Manual map construction | β Two powerful strategies |
| Generic Support | β No generic handling | β Full generic models |
| Aggregations | β Basic count only | β One-shot count/sum/average (server-side) |
| Pagination | β Manual, risky | β Smart Builder, zero risk |
| Transactions | β Manual read-before-write | β Automatic deferred writes |
| Runtime Errors | β Common | β Eliminated at compile-time |
Lightning Fast Code Generation #
- π Inline-first optimized - Callables and Dart extensions for maximum performance
- π¦ Small generated surface - one unified codegen pipeline per model
- β‘ Measured performance - recorded benchmark harness in CI
- π Model reusability - Same model works in collections and subcollections
- β±οΈ Sub-second generation - Complex schemas compile in under 1 second
- π― Zero runtime overhead - All magic happens at compile time (no reflection)
Revolutionary Features #
Smart Builder Pagination - Eliminates common Firestore pagination bugs:
// Get first page with ordering
final page1 = await db.users
.orderBy(($) => ($.followers(descending: true), $.name()))
.limit(10)
.get();
// Get next page with perfect type-safety - zero inconsistency risk
final page2 = await db.users
.orderBy(($) => ($.followers(descending: true), $.name()))
.startAfterObject(page1.last) // Auto-extracts cursor values
.limit(10)
.get();
One-shot Server-Side Aggregations (ADR-0002 β no fake client-side
streaming; server-side aggregate streams are added only when
cloud_firestore exposes them):
final stats = await db.users
.where(($) => $.isActive(isEqualTo: true))
.aggregate(($) => (
count: $.count(),
averageAge: $.age.average(),
totalFollowers: $.profile.followers.sum(),
))
.get();
print('${stats.count} users, avg ${stats.averageAge}');
---
## π₯ Before vs After
### Smart Query Building
```dart
// β Standard - String-based field paths, typos cause runtime errors
final result = await FirebaseFirestore.instance
.collection('users')
.where('isActive', isEqualTo: true)
.where('profile.followers', isGreaterThan: 100)
.where('age', isLessThan: 30)
.get();
// β
ODM - Type-safe query builder with IDE support
final result = await db.users
.where(($) =>
$.isActive(isEqualTo: true) &
$.profile.followers(isGreaterThan: 100) &
$.age(isLessThan: 30),
)
.get();
Intelligent Updates #
// β Standard - Manual map construction, error-prone
await userDoc.update({
'profile.followers': FieldValue.increment(1),
'tags': FieldValue.arrayUnion(['verified']),
'lastLogin': FieldValue.serverTimestamp(),
});
// β
ODM - Explicit typed patch operations (ADR-0002)
await userDoc.patch((p) => [
p.profile.followers.increment(1),
p.age.increment(1),
p.tags.arrayUnion(['premium', 'active']), // atomic array union
p.scores.arrayRemove([0, -1]), // atomic array remove
p.lastLogin.serverTimestamp(), // server-set time
p.name.set('Renamed'), // plain set
p.oldField.delete(), // field delete
]);
Read-modify-write belongs in transactions, where it is safe:
await db.runTransaction((tx) async {
final txUsers = db.users.inTransaction(tx);
final user = await txUsers('jane').get();
txUsers('jane').patch((p) => [p.age.increment(1)]);
});
π¦ Installation #
1. Add Dependencies #
dart pub add firestore_odm
dart pub add dev:firestore_odm_builder
dart pub add dev:build_runner
You'll also need a JSON serialization solution:
# If using Freezed (recommended)
dart pub add freezed_annotation
dart pub add dev:freezed
dart pub add dev:json_serializable
# If using plain classes
dart pub add json_annotation
dart pub add dev:json_serializable
2. Configure json_serializable (Critical for Nested Models) #
β οΈ Important: If you're using models with nested objects (especially with Freezed), you must create a build.yaml file next to your pubspec.yaml:
# build.yaml
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: true
Why is this required? Without this configuration, json_serializable generates broken toJson() methods for nested objects. Instead of proper JSON, you'll get Instance of 'NestedClass' stored in Firestore, causing data corruption and deserialization failures.
When you need this:
- β Using nested Freezed classes
- β
Using nested objects with
json_serializable - β Working with complex object structures
- β Encountering "Instance of..." in Firestore console
Alternative: Add @JsonSerializable(explicitToJson: true) to individual classes if you can't use global configuration.
π Quick Start #
1. Define Your Model #
// lib/models/user.dart
import 'package:firestore_odm_annotation/firestore_odm_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
@DocumentIdField() required String id,
required String name,
required String email,
required int age,
DateTime? lastLogin,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
2. Define Your Schema #
// lib/schema.dart
import 'package:firestore_odm_annotation/firestore_odm_annotation.dart';
import 'models/user.dart';
part 'schema.odm.dart';
@Schema()
@Collection<User>("users")
final appSchema = _$AppSchema;
3. Generate Code #
dart run build_runner build --delete-conflicting-outputs
4. Start Using #
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firestore_odm/firestore_odm.dart';
import 'schema.dart';
final firestore = FirebaseFirestore.instance;
final db = FirestoreODM(appSchema, firestore: firestore);
// Create a user with an explicit ID (full replace)
await db.users.set(User(
id: 'jane',
name: 'Jane Smith',
email: 'jane@example.com',
age: 28,
));
// Create a user with a server-generated ID (the ID is returned)
final id = await db.users.create(User(
id: '',
name: 'John Doe',
email: 'john@example.com',
age: 30,
));
// Get a user
final user = await db.users('jane').get();
print(user?.name); // "Jane Smith"
// Type-safe queries
final youngUsers = await db.users
.where(($) => $.age(isLessThan: 30))
.orderBy(($) => $.name())
.get();
π Advanced Features #
Subcollections with Model Reusability #
@Schema()
@Collection<User>("users")
@Collection<Post>("posts")
@Collection<Post>("users/*/posts") // Same Post model, different location
final appSchema = AppSchema();
// Access a user's posts (path-derived accessor: users/*/posts -> usersPosts)
final userPosts = db.usersPosts('jane');
await userPosts.set(Post(id: 'post1', title: 'Hello World!'));
Bulk Operations (chunked to Firestore's 500-write limit) #
// Patch every match with the same typed operations
await db.users
.where(($) => $.isPremium(isEqualTo: true))
.patchAll([IncrementOperation(const FieldNode(components: ['points']), 100)]);
// Delete every match
await db.users
.where(($) => $.isActive(isEqualTo: false))
.deleteAll();
Smart Transactions #
await db.runTransaction((tx) async {
// All reads happen first automatically
final sender = await tx.users('user1').get();
final receiver = await tx.users('user2').get();
// Writes are automatically deferred until the end
tx.users('user1').patch(($) => [$.balance.increment(-100)]);
tx.users('user2').patch(($) => [$.balance.increment(100)]);
});
Atomic Batch Operations (typed create/set/patch/delete) #
// Automatic management - simple and clean
await db.runBatch((batch) {
final users = db.users.inBatch(batch);
users.set(newUser);
db.posts.inBatch(batch).set(existingPost);
db.usersPosts('user_id').inBatch(batch).set(userPost);
users.delete('old_user');
});
// Manual management - fine-grained control
final batch = db.batch();
db.users.inBatch(batch).set(user1);
db.users.inBatch(batch).set(user2);
db.posts.inBatch(batch).patch('p1', (p) => [p.likes.increment(1)]);
await batch.commit();
Server Timestamps & Generated IDs (no sentinels, ADR-0002) #
// Server timestamps are explicit patch operations
await userDoc.patch((p) => [p.updatedAt.serverTimestamp()]);
// Server-generated document IDs come from create()
final id = await db.users.create(User(
id: '',
name: 'John Doe',
email: 'john@example.com',
));
Server-set times use the explicit patch op (ADR-0002 β no sentinels):
patch((p) => [p.updatedAt.serverTimestamp()]).
π Performance & Technical Excellence #
Optimized Code Generation #
| Metric | How it is measured |
|---|---|
| Serialization round-trip | recorded BENCH serialization_roundtrip_us_per_op |
| Patch operation latency | recorded BENCH patch_op_ms_per_op |
| Runtime overhead | zero reflection β all magic at compile time |
Claims without measurements are not made; the harness runs in the
performance CI lane (apps/flutter_example/test/benchmarks_test.dart).
Advanced Capabilities #
- β
Complex logical operations -
and()andor() - β
Array operations -
arrayContains,arrayContainsAny,whereIn - β Range queries - Proper ordering constraints
- β Nested field access - Full type safety
- β Transaction support - Automatic deferred writes
- β Query/document streams - Real-time updates
- β Error handling - Meaningful compile-time messages
- β
Testing support -
fake_cloud_firestoreintegration
Flexible Data Modeling #
freezed(recommended) - Robust immutable classesjson_serializable- Plain Dart classes with full controlfast_immutable_collections- High-performanceIList,IMap,ISet
π§ͺ Testing #
Perfect integration with fake_cloud_firestore:
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('user operations work correctly', () async {
final firestore = FakeFirebaseFirestore();
final db = FirestoreODM(appSchema, firestore: firestore);
await db.users.set(User(
id: 'test',
name: 'Test User',
email: 'test@example.com',
age: 25,
));
final user = await db.users('test').get();
expect(user?.name, 'Test User');
});
}
πΊοΈ Roadmap #
β Completed (3.0 β 4.0)
- β Full generic model support
- β Honest JsonKey subset + @JsonConverter
- β Recorded benchmark harness (no unmeasured perf claims)
- β
Enum support β string & numeric
@JsonValue,orderBy, defaults (4.0) - β Automatic nested-class imports (4.0)
- β Batch & transaction patch builders (4.0)
- β Server timestamps on insert (4.0)
- β Production-ready stability
π Next
- β Firestore Pipelines support
- β Full map field filtering, ordering, and aggregation
- β Nested map support
- β Enhanced documentation
π€ Support #
- π Bug Reports
- π¬ Discussions
- π Full Documentation
- π§ Email
Show Your Support: β Star β’ π Watch β’ π Report bugs β’ π‘ Suggest features β’ π Contribute
π License #
MIT Β© Sylphx
π Credits #
Built with:
- Freezed - Immutable classes
- json_serializable - JSON serialization
- build_runner - Code generation
Special thanks to the Flutter and Dart communities β€οΈ
Zero reflection. Type-safe. Production-ready.
The Firestore ODM that actually scales
sylphx.com β’
@SylphxAI β’
hi@sylphx.com