Dominik Roszkowski
Dominik Roszkowski

Reputation: 2543

How to deserialize Firestore document with its id using json_serializable in Flutter?

I have a simple Message document in my Firestore database that has some fields.

enter image description here

I use json_serializable to deserialize it to object. My class looks like follows:

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

part 'message_firestore.g.dart';

@JsonSerializable(nullable: true, explicitToJson: true)
class MessageFirestore extends Equatable {
  MessageFirestore(
      this.id, this.content, this.conversationId, this.senderId, this.dateSent);

  factory MessageFirestore.fromJson(Map<String, dynamic> json) =>
      _$MessageFirestoreFromJson(json);

  Map<String, dynamic> toJson() => _$MessageFirestoreToJson(this);

  @JsonKey(name: 'Id')
  final String id;
  @JsonKey(name: 'Content')
  final String content;
  @JsonKey(name: 'ConversationId')
  final String conversationId;
  @JsonKey(name: 'SenderId')
  final String senderId;
  @JsonKey(name: 'DateSent', fromJson: _fromJson, toJson: _toJson)
  final DateTime dateSent;

  static DateTime _fromJson(Timestamp val) =>
      DateTime.fromMillisecondsSinceEpoch(val.millisecondsSinceEpoch);
  static Timestamp _toJson(DateTime time) =>
      Timestamp.fromMillisecondsSinceEpoch(time.millisecondsSinceEpoch);
}

There is no field called Id in the document, so currently its id is not being deserialized. However, the key of the map retrieved from Firestore is its id, so this value can be read by manually deserializing the map. I wish to have access to the id of the document (_b03002...) during deserialization.

Is there any way to configure json_serializable to read this id and store it in id property?

Upvotes: 6

Views: 4702

Answers (4)

Adnan
Adnan

Reputation: 1281

using Yaobin Then's answer, we can improve it forward like this:

factory MessageFirestore.fromJson(String id, Map<String, dynamic> json) {
  return _$MessageFirestoreFromJson(json)..id = id;
}

factory MessageFirestore.fromFire(QueryDocumentSnapshot snapshot) {
  return MessageFirestore.fromJson(snapshot.id, snapshot.data() as Map<String, dynamic>);
}

Then, from your caller, it'll be something like this

return StreamBuilder<QuerySnapshot>(
  stream: ...,
  builder: (context, snp) {
    final products = snp.data?.docs;

    if (products?.isNotEmpty != true) {
      return const Center(
        child: Text('No products'),
      );
    }

    final prods = products!.map(
      (prod) {
        return MessageFirestore.fromFire(prod);
      },
    ).toList();

this way you do not have to fill the 2 arguments in every call, the fromFire factory will handle it for you

Upvotes: 0

Nicolas Degen
Nicolas Degen

Reputation: 1846

Improvement to Yaobin Then's post: Also remove id on toJson:


factory MessageFirestore.fromJson(String id, Map<String, dynamic> json) {
  return _$MessageFirestoreFromJson(json)..id = id;
}

Map<String, dynamic> toJson() {
  var json = _$MessageFirestoreToJson(this);
  json.removeWhere((key, value) => key == 'id');
  return json;
}

Upvotes: 2

Tofiq Samali
Tofiq Samali

Reputation: 163

You could add another factory into MessageFirestore class.

  factory MessageFirestore.fromFire(DocumentSnapshot doc) =>
      _$MessageFirestoreFromFire(doc);

after that you will have two factory function in you class.

factory MessageFirestore.fromFire(DocumentSnapshot doc) //...

factory MessageFirestore.fromJson(Map<String, dynamic> json) //...

and add _$MessageFirestoreFromFire(doc) function with copying _$MessageFirestoreFromJson(json) function into message_firestore.g.dart file and edit it like this:

MessageFirestore _$MessageFirestoreFromFire(DocumentSnapshot doc) {
  return MessageFirestore(
    id: doc.documentID,
    content: doc.data['name'] as String,
    // ... other parameters 
  );
}

and in you service to reading the documents you can:

  Stream<List<MessageFirestore>> getMessageList() {
    return Firestore.instance
        .collection('YourCollectionPath')
        .snapshots()
        .map((snapShot) => snapShot.documents
            .map(
              (document) => MessageFirestore.fromFire(document),
            )
            .toList());
  }

Easy Peasy

And also this method doesn't interference with other classes that use MessageFirestore instance.

Have a nice day and wish this method works for you. :)

Upvotes: 2

Yaobin Then
Yaobin Then

Reputation: 2832

You could modify the fromJson constructor, so that you'd provide the id on the first parameter.

factory MessageFirestore.fromJson(String id, Map<String, dynamic> json) {
  return _$MessageFirestoreFromJson(json)..id = id;
}

Then, from your caller, it'll be something like this

Message(snapshot.documentID, snapshot.data)

Upvotes: 14

Related Questions