Type-safe Firestore ODM for Dart/Flutter - zero reflection, code generation
20% faster runtime β’ 15% less code β’ Zero reflection β’ Full type safety
Documentation β’ Getting Started β’ Examples
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 fragileThe 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 accessResult: Zero reflection, 20% faster runtime, eliminate runtime errors.
The most stable and feature-complete release yet - over 90% of planned features complete!
| Metric | Improvement | Impact |
|---|---|---|
| Runtime Performance | 20% faster | Optimized code generation |
| Generated Code | 15% smaller | Extension-based architecture |
| Compilation Speed | <1 second | Complex schemas compile instantly |
| Runtime Overhead | Zero | All magic at compile time |
- β Full generic model support - Generic classes with type-safe patch operations
- β Complete JsonKey & JsonConverter support - Full serialization control
- β Automatic conversion fallbacks - JsonConverter optional in most cases
- β Enhanced map operations - Comprehensive map field support with atomic ops
- β 100+ new test cases - Rigorous testing for production stability
| 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 | β Comprehensive + streaming |
| Pagination | β Manual, risky | β Smart Builder, zero risk |
| Transactions | β Manual read-before-write | β Automatic deferred writes |
| Runtime Errors | β Common | β Eliminated at compile-time |
- π Inline-first optimized - Callables and Dart extensions for maximum performance
- π¦ 15% less generated code - Smart generation without bloating your project
- β‘ 20% performance improvement - Optimized runtime execution
- π 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
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();Streaming Aggregations - Real-time aggregation subscriptions:
// Live statistics that update in real-time
db.users
.where(($) => $.isActive(isEqualTo: true))
.aggregate(($) => (
count: $.count(),
averageAge: $.age.average(),
totalFollowers: $.profile.followers.sum(),
))
.stream
.listen((stats) {
print('Live: ${stats.count} users, avg age ${stats.averageAge}');
});// β 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(($) => $.and(
$.isActive(isEqualTo: true),
$.profile.followers(isGreaterThan: 100),
$.age(isLessThan: 30),
))
.get();// β Standard - Manual map construction, error-prone
await userDoc.update({
'profile.followers': FieldValue.increment(1),
'tags': FieldValue.arrayUnion(['verified']),
'lastLogin': FieldValue.serverTimestamp(),
});// β
ODM - Two powerful update strategies
// 1. Patch - Explicit atomic operations (Best Performance)
await userDoc.patch(($) => [
$.profile.followers.increment(1),
$.tags.add('verified'), // Add single element
$.tags.addAll(['premium', 'active']), // Add multiple elements
$.scores.removeAll([0, -1]), // Remove multiple elements
$.lastLogin.serverTimestamp(),
]);
// 2. Modify - Smart atomic detection (Read + Auto-detect operations)
await userDoc.modify((user) => user.copyWith(
age: user.age + 1, // Auto-detects -> FieldValue.increment(1)
tags: [...user.tags, 'expert'], // Auto-detects -> FieldValue.arrayUnion()
lastLogin: FirestoreODM.serverTimestamp,
));dart pub add firestore_odm
dart pub add dev:firestore_odm_builder
dart pub add dev:build_runnerYou'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_serializablebuild.yaml file next to your pubspec.yaml:
# build.yaml
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: trueWhy 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.
// 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);
}// 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;dart run build_runner build --delete-conflicting-outputsimport '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 custom ID
await db.users.insert(User(
id: 'jane',
name: 'Jane Smith',
email: 'jane@example.com',
age: 28,
));
// Create a user with auto-generated ID
await db.users.insert(User(
id: FirestoreODM.autoGeneratedId,
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();@Schema()
@Collection<User>("users")
@Collection<Post>("posts")
@Collection<Post>("users/*/posts") // Same Post model, different location
final appSchema = _$AppSchema;
// Access user's posts
final userPosts = db.users('jane').posts;
await userPosts.insert(Post(id: 'post1', title: 'Hello World!'));// Update all premium users using patch (best performance)
await db.users
.where(($) => $.isPremium(isEqualTo: true))
.patch(($) => [$.points.increment(100)]);
// Update all premium users using modify (read + auto-detect atomic)
await db.users
.where(($) => $.isPremium(isEqualTo: true))
.modify((user) => user.copyWith(points: user.points + 100));
// Delete inactive users
await db.users
.where(($) => $.status(isEqualTo: 'inactive'))
.delete();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)]);
});// Automatic management - simple and clean
await db.runBatch((batch) {
batch.users.insert(newUser);
batch.posts.update(existingPost);
batch.users('user_id').posts.insert(userPost);
batch.users('old_user').delete();
});
// Manual management - fine-grained control
final batch = db.batch();
batch.users.insert(user1);
batch.users.insert(user2);
batch.posts.update(post);
await batch.commit();// Server timestamps using patch (best performance)
await userDoc.patch(($) => [$.lastLogin.serverTimestamp()]);
// Server timestamps using modify (read + smart detection)
await userDoc.modify((user) => user.copyWith(
loginCount: user.loginCount + 1, // Uses current value + auto-detects increment
lastLogin: FirestoreODM.serverTimestamp,
));
// Auto-generated document IDs
await db.users.insert(User(
id: FirestoreODM.autoGeneratedId, // Server generates unique ID
name: 'John Doe',
email: 'john@example.com',
));FirestoreODM.serverTimestamp must be used exactly as-is. Any arithmetic operations (+, .add(), etc.) will create a regular DateTime instead of a server timestamp. See the Server Timestamps Guide for alternatives.
| Metric | Value | Benefit |
|---|---|---|
| Runtime Performance | +20% | Optimized execution paths |
| Generated Code Size | -15% | Smart generation without bloat |
| Compilation Time | <1 second | Complex schemas compile instantly |
| Runtime Overhead | Zero | All magic at compile time |
- β
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
- β Streaming subscriptions - Real-time updates
- β Error handling - Meaningful compile-time messages
- β
Testing support -
fake_cloud_firestoreintegration
freezed(recommended) - Robust immutable classesjson_serializable- Plain Dart classes with full controlfast_immutable_collections- High-performanceIList,IMap,ISet
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.insert(User(
id: 'test',
name: 'Test User',
email: 'test@example.com',
age: 25
));
final user = await db.users('test').get();
expect(user?.name, 'Test User');
});
}β Completed (v3.0)
- Full generic model support
- Complete JsonKey & JsonConverter support
- 20% runtime performance improvement
- 15% reduction in generated code
- 100+ new test cases
- Production-ready stability
π Next
- Batch collection operations
- Map field filtering, ordering, and aggregation
- Nested map support
- Performance monitoring
- Enhanced documentation
- π Bug Reports
- π¬ Discussions
- π Full Documentation
- π§ Email
Show Your Support: β Star β’ π Watch β’ π Report bugs β’ π‘ Suggest features β’ π Contribute
MIT Β© Sylphx
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