23

I want to fetch meetings from Firestore and map them into the following Meeting model:

part 'meeting.g.dart';

@JsonSerializable(explicitToJson: true)
class Meeting {
  String id;
  DateTime date;

  Meeting(this.id, this.date);

  factory Meeting.fromJson(Map<String, dynamic> json) {

    return _$MeetingFromJson(json);
  }

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

The documents are fetched from Firestore and then fromJson is called on the iterable, but an exception is thrown:

type 'Timestamp' is not a subtype of type 'String' in type cast

When I go into generated meeting.g.dart, it's this line which causes the error

json['date'] == null ? null : DateTime.parse(json['date'] as String)

To workaround the issue, I've tried changing from DateTime to Timestamp in the model, but then the following build error is shown:

Error running JsonSerializableGenerator
Could not generate `fromJson` code for `date`.
None of the provided `TypeHelper` instances support the defined type.

Could you tell me how do you solve this issue? Is there another preferred way to combine Firebase and a Flutter project using json_serializable for JSON serialization? Maybe even replace usage of json_serializable ?

Reed
  • 1,161
  • 13
  • 25
  • Have you tried replacing `DateTime.parse(json['date'] as String)`. With `DateTime.parse(json['date'].toString())` – Gabe Mar 21 '20 at 21:27
  • I don't think it's a good practice to edit generated files. I would have to do this every time the model changes, because the meeting.g.dart would be re-generated. – Reed Mar 21 '20 at 21:30
  • 1
    You might have to serialize the json manually, take a look at this post https://stackoverflow.com/a/58309472/9609442 – Gabe Mar 21 '20 at 21:40
  • In app in production I parse dateline to string before upload it, then convert it back to date time when fetch it. – i6x86 Mar 22 '20 at 01:41
  • What are you getting in json['date'] ? Is it millisecondsSinceEpoch or dateString? – Suman Maharjan Mar 22 '20 at 11:27
  • Could you please try with the following : json['date'].toDate() and let me know if it works ? – Nibrass H Mar 23 '20 at 11:17
  • How did you define date typed field in dart model? Kindly suggest. Thanks. – Kamlesh Jun 15 '21 at 13:26

4 Answers4

38

Use JsonConverter

class TimestampConverter implements JsonConverter<DateTime, Timestamp> {
  const TimestampConverter();

  @override
  DateTime fromJson(Timestamp timestamp) {
    return timestamp.toDate();
  }

  @override
  Timestamp toJson(DateTime date) => Timestamp.fromDate(date);
}

@JsonSerializable()
class User{
  final String id;
  @TimestampConverter()
  final DateTime timeCreated;

  User([this.id, this.timeCreated]);

  factory User.fromSnapshot(DocumentSnapshot documentSnapshot) =>
      _$UserFromJson(
          documentSnapshot.data..["_id"] = documentSnapshot.documentID);

  Map<String, dynamic> toJson() => _$UserToJson(this)..remove("_id");
}
Junsu Lee
  • 1,481
  • 11
  • 7
  • Worked like a charm! Thanks. This is also compatible with [freezed](https://pub.dev/packages/freezed). Other solutions above are overcomplicating the issue. – Alex Hartford Sep 10 '20 at 20:52
  • @ValentinSeehausen You are able to use the @TimestampConverter() decorator with a freezed class just like you can with a JsonSerializable class. It works exactly the same way. – Alex Hartford Apr 19 '21 at 14:43
  • 1
    @AlexHartford Thanks for the answer. Did you test it with the current, nullsafe versions? Somehow it did not work for me. – Valentin Seehausen Apr 19 '21 at 19:13
  • @ValentinSeehausen Yes, can you open a question and post your code? I would be happy to take a look. – Alex Hartford Apr 20 '21 at 14:55
  • Hey @AlexHartford, thanks for you support. I copied a question at [GitHub](https://github.com/rrousselGit/freezed/issues/15#issuecomment-822482887) and posted it [here](https://stackoverflow.com/questions/67150713/freezed-and-json-serializable-how-to-use-a-custom-converter). Looking forward to your comments. – Valentin Seehausen Apr 20 '21 at 16:00
  • How did you defined date/timestamp typed field in dart model with NULL Safety? Kindly suggest. Thanks. – Kamlesh Jun 15 '21 at 13:28
8

Thanks to @Reed, for pointing to the right direction. When passing DateTime value to FireStore there seem no issues for firebase to take that value as Timestamp, however when getting it back it needs to be properly handled. Anyways, here is example, that works both ways:

import 'package:cloud_firestore/cloud_firestore.dart'; //<-- dependency referencing Timestamp
import 'package:json_annotation/json_annotation.dart';

part 'test_date.g.dart';

@JsonSerializable(anyMap: true)
class TestDate {

  @JsonKey(fromJson: _dateTimeFromTimestamp, toJson: _dateTimeAsIs)
  final DateTime theDate; 


  TestDate({this.theDate,});

   factory TestDate.fromJson(Map<String, dynamic> json) {     
     return _$TestDateFromJson(json);
   } 
  Map<String, dynamic> toJson() => _$TestDateToJson(this);

  static DateTime _dateTimeAsIs(DateTime dateTime) => dateTime;  //<-- pass through no need for generated code to perform any formatting

// https://stackoverflow.com/questions/56627888/how-to-print-firestore-timestamp-as-formatted-date-and-time-in-flutter
  static DateTime _dateTimeFromTimestamp(Timestamp timestamp) {
    return DateTime.parse(timestamp.toDate().toString());
  }
}
mike123
  • 1,549
  • 15
  • 23
7

Solution #1

Use toJson and fromJson converter functions as in the following example: https://github.com/dart-lang/json_serializable/blob/master/example/lib/example.dart

Benefits of the solution is that you don't have to hard-code property names

Solution #2

After reading https://github.com/dart-lang/json_serializable/issues/351, I've changed Meeting.fromJson and it works as expected now:

  factory Meeting.fromJson(Map<String, dynamic> json) {
    json["date"] = ((json["date"] as Timestamp).toDate().toString());
    return _$MeetingFromJson(json);
  }

json["date"] is Timestamp by default, I convert it to String, before it reaches the generated deserializer, so it doesn't crash when it tries to cast json["date"] as String

Though, I don't like this workaround very much, because I have to hard-code property's name and couple to types, but for now, this solution will be good enough.

An alternative would be to try out https://pub.dev/packages/built_value for serialiazion, which is recommended in their blog https://flutter.dev/docs/development/data-and-backend/json

Community
  • 1
  • 1
Reed
  • 1,161
  • 13
  • 25
  • Glad it helped. Have you tried the solution from this example https://github.com/dart-lang/json_serializable/blob/master/example/lib/example.dart ? – Reed Mar 27 '20 at 09:04
  • 1
    Had to go with the `@JsonKey(fromJson: _dateTime, toJson: _dateTime)` to simply letting the `json_annotation` library ignore `DateTime` massaging. That way firestore will properly handle this field as timestamp. `static DateTime _dateTime(DateTime dateTime) => dateTime;` – mike123 Mar 29 '20 at 21:08
  • How did you defined date/timestamp typed field in dart model? Kindly suggest. Thanks. – Kamlesh Jun 15 '21 at 13:27
  • Timestamp is a type from cloud_firestore library – Reed Jun 17 '21 at 04:47
0

I had the same issues, but json_serializer still wasn't converting the Timestamp objects because some of them were nullable.

// nullable
class TimestampConverter implements JsonConverter<DateTime?, Timestamp?> {
  const TimestampConverter();

  @override
  DateTime? fromJson(Timestamp? timestamp) => timestamp?.toDate();

  @override
  Timestamp? toJson(DateTime? date) => date == null ? null : Timestamp.fromDate(date);
}

Also, I like to put the additional id field in the stream method so it looks cleaner in the class. In my case, I just save the DocumentReference.

Stream<User> streamUser() {
  return user().snapshots().map(
    (snapshot) {
      try {
        return User.fromDocument(snapshot);
      } catch (e) {
        FirebaseWorker().signOut();
        rethrow;
      }
    },
  );
}

Lastly, I had multiple DateTime properties so I was able to put @TimestampConverter at the top of the class.

And you can ignore properties so that it doesn't get serialized when calling toJson with @JsonKey(ignore: true).

So final code looks like this:

@TimestampConverter() // <--
class User{
  @JsonKey(ignore: true) // <--
  final String id;
  final DateTime? birthday;
  final DateTime timeCreated;

  User([this.id, this.timeCreated]);

  factory UserProfile.empty() => UserProfile(id: '', timeCreated: DateTime.now());

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

  factory UserProfile.fromDocument(DocumentSnapshot documentSnapshot) 
  {
    final data = documentSnapshot.data();
    return data != null
        ? UserProfile.fromJson(data as Map<String, dynamic>)
            ..reference = documentSnapshot.reference
        : UserProfile.empty();

  Map<String, dynamic> toJson() => _$UserToJson(this); // <-- don't need to remove id anymore
  }
1housand
  • 508
  • 7
  • 16