Skip to content

Migrating from 4.x to 5.0 ​

5.0 changes most call sites. Each ODM method now maps to one cloud_firestore call, and DateTime fields are stored as Firestore Timestamp values. There is no compatibility layer; update the code and, if you stored dates with 4.x, convert the stored data.

Steps ​

  1. Set firestore_odm and firestore_odm_builder to ^5.0.0.
  2. Update the schema declaration (below).
  3. Convert stored DateTime strings to Timestamp values (below).
  4. Replace removed APIs using the tables below.
  5. Regenerate: dart run build_runner build --delete-conflicting-outputs.
  6. Run your tests against the emulator or a test database.

Schema declaration ​

4.x generated the schema class. In 5.0 you declare it:

dart
// 4.x
part 'schema.g.dart';

@Schema()
@Collection<User>('users')
const appSchema = _$AppSchema;
dart
// 5.0
part 'schema.g.dart';

class AppSchema extends FirestoreSchema {
  const AppSchema();
}

@Schema()
@Collection<User>('users')
const appSchema = AppSchema();

Stored dates ​

4.x wrote DateTime fields as ISO-8601 strings. 5.0 writes and reads them as Timestamp values, and reading a document that still holds a string in a DateTime field throws. Convert existing documents once, before the new app version reads them. A sketch with cloud_firestore, for the users collection:

dart
import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> convertDates(
  FirebaseFirestore db,
  String collection,
  List<String> dateFields,
) async {
  final docs = (await db.collection(collection).get()).docs;
  for (var i = 0; i < docs.length; i += 500) {
    final batch = db.batch();
    for (final doc in docs.skip(i).take(500)) {
      final updates = <String, Object?>{};
      for (final field in dateFields) {
        final value = doc.data()[field];
        if (value is String) {
          updates[field] = Timestamp.fromDate(DateTime.parse(value));
        }
      }
      if (updates.isNotEmpty) batch.update(doc.reference, updates);
    }
    await batch.commit();
  }
}

await convertDates(FirebaseFirestore.instance, 'users', [
  'lastLogin',
  'createdAt',
  'updatedAt',
]);

Nested date fields use dotted paths, such as 'profile.lastActive'. Run the conversion for every collection and subcollection with DateTime fields. Queries that filter or sort on those fields only match converted documents.

Duration is stored as integer microseconds in both versions; no change is needed.

Writes ​

4.x5.0
insert(model)set(model) to write under the model's ID, or create(model) for a generated ID
insert(User(id: FirestoreODM.autoGeneratedId, ...))final id = await create(model)
update(model)set(model) (replaces; does not require the document to exist)
upsert(model)set(model)
modify((u) => u.copyWith(...))patch(($) => [...]), or a transaction when the new value depends on the current one
incrementalModify(...)patch(($) => [...]) with increment, arrayUnion, arrayRemove
exists()await get() != null

Patch operations ​

4.x5.0
$.name('x')$.name.set('x')
$.tags.add(v) / $.tags.addAll([...])$.tags.arrayUnion([...])
$.tags.remove(v) / $.tags.removeAll([...])$.tags.arrayRemove([...])
$.age.increment(1)$.age.increment(1) (unchanged)
$.updatedAt.serverTimestamp()$.updatedAt.serverTimestamp() (unchanged)
FirestoreODM.serverTimestamp in a model$.field.serverTimestamp() in a patch (see Server Timestamps)
$.profile.followers.increment(1) (nested field)$.profile.set(updatedProfile) (nested models are patched as a whole value)
$.settings.setKey('k', v)$.settings.set({...current, 'k': v})

Other changes:

  • patch sends exactly the listed operations. 4.x merged operations on the same field by precedence rules; 5.0 does not. If you list two operations on one field, Firestore applies the last one.
  • increment is available on non-nullable numeric fields, and arrayUnion / arrayRemove on non-nullable List fields.

Queries ​

4.x5.0
where(($) => $.and(a, b))where(($) => a & b) or a.and(b)
where(($) => $.or(a, b))where(($) => a | b) or a.or(b)
orderBy(($) => $.age())orderBy(($) => ($.age(),)) (always a record)
count().get()count() returns Future<int>
count().stream, aggregate(...).streamremoved; call count() or aggregate(...).get() again
query.patch(($) => [...])query.patchAll([...]) with operations from <Model>PatchBuilder() (see Bulk Operations)
query.modify(...), query.incrementalModify(...)query.patchAll([...])
query.delete()query.deleteAll()

Subcollections, transactions and batches ​

4.x5.0
odm.users('u1').postsodm.usersPosts('u1')
odm.users('u1').posts('p1').commentsodm.usersPostsComments('u1', 'p1')
tx.users('u1').get()odm.users.inTransaction(tx)('u1').get()
tx.users('u1').modify(...)odm.users.inTransaction(tx)('u1').patch(($) => [...])
batch.users.insert(model)odm.users.inBatch(batch).set(model) or .create(model)
batch.users('u1').patch(...)odm.users.inBatch(batch).patch('u1', ...)
batch.users('u1').delete()odm.users.inBatch(batch).delete('u1')

Models ​

  • fast_immutable_collections types (IList, IMap, ISet) are not supported. Use List, Map and Set.
  • json_serializable is no longer needed for storage. The ODM generates its own converters; keep toJson/fromJson only if you use them elsewhere.

New since 4.x ​

  • create returns the generated document ID, also in batches and transactions.
  • get accepts GetOptions to read from the cache or the server only.
  • patchAll and deleteAll split their writes into batches of at most 500.
  • GeoPoint, DocumentReference, Blob and Timestamp fields are stored as they are.

See Benchmarks for performance measurements.