How do I resolve type 'Timestamp' is not a subtype of type 'String' in type cast

Viewed 6210

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 ?

4 Answers

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");
}

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());
  }
}

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

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
  }
Related