k-paxian / dart-json-mapper

Serialize / Deserialize Dart Objects to / from JSON
https://pub.dev/packages/dart_json_mapper
Other
400 stars 33 forks source link

Error while using this with Flutter Firestore #34

Closed ScottPierce closed 4 years ago

ScottPierce commented 4 years ago

I'm using this library to deserialize Firestore documents, and I'm seeing the following error whenever I try to call JsonMapper.fromMap<T>(data);

E/flutter (23945): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: type '_ImmutableMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'
E/flutter (23945): null
E/flutter (23945): [ERROR:flutter/shell/common/shell.cc(199)] Dart Error: Unhandled exception:
E/flutter (23945): type '_ImmutableMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'
E/flutter (23945): #0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1114:29)
E/flutter (23945): #1      _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
E/flutter (23945): #2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)

I was seeing a similar issue with json_serializable, but I was able to fix it with an option they provided in my build.yaml: https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonSerializable/anyMap.html

ScottPierce commented 4 years ago

It seems to be unhappy with this model:

@jsonSerializable
class Registration {
  final String id;
  final String type;
  final Person student;
  final List<Person> guardians;
  final Map<String, String> addresses;
  final bool isRead;
  final bool isArchived;

  Registration(this.id, this.type, this.student, {this.guardians = const [], this.addresses = const {}, this.isRead = false, this.isArchived = false});

  Registration copy({String id, String type, Person student, List<Person> guardians, Map<String, String> addresses, bool isRead, bool isArchived}) {
    return Registration(
      id ?? this.id,
      type ?? this.type,
      student ?? this.student,
      guardians: guardians ?? this.guardians,
      addresses: addresses ?? this.addresses,
      isRead: isRead ?? this.isRead,
      isArchived: isArchived ?? this.isArchived,
    );
  }

  @override
  String toString() {
    return 'Registration{id: $id, type: $type, student: $student, guardians: $guardians, address: $addresses, isRead: $isRead, isArchived: $isArchived}';
  }
}
ScottPierce commented 4 years ago

So I'm seeing several layers of problems. Going to try and summarize them here.

I have a function to grab the firebase firestore document and try to deserialize it. Here's what it looks like:

class FirebaseMapper {
  static T fromDocument<T>(DocumentSnapshot document) {
    final data = document.data.cast<String, dynamic>();
    data['id'] = document.documentID;
    return JsonMapper.fromJson<T>(data);
  }

  FirebaseMapper._();
}

Only, when I do that, I'm seeing this error:

E/flutter (25770): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'
E/flutter (25770): null
E/flutter (25770): [ERROR:flutter/shell/common/shell.cc(199)] Dart Error: Unhandled exception:
E/flutter (25770): type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'
E/flutter (25770): #0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1114:29)
E/flutter (25770): #1      _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
E/flutter (25770): #2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)

So I try to work around it using this:

  static T fromDocument<T>(DocumentSnapshot document) {
    final data = document.data.cast<String, dynamic>();
    data['id'] = document.documentID;

    final json = jsonEncode(data);
    return JsonMapper.fromJson<T>(json);
  }

Then I get this error from this Registration model:

E/flutter (25770): [ERROR:flutter/shell/common/shell.cc(199)] Dart Error: Unhandled exception:
E/flutter (25770): type '_ImmutableMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'
E/flutter (25770): #0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1114:29)
E/flutter (25770): #1      _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
E/flutter (25770): #2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)
@jsonSerializable
class Registration {
  final String id;
  final String type;
  final Person student;
  final List<Person> guardians;
  final Map<String, String> addresses;
  final bool isRead;
  final bool isArchived;

  Registration(this.id, this.type, this.student, {this.guardians = const [], this.addresses = const {}, this.isRead = false, this.isArchived = false});

  Registration copy({String id, String type, Person student, List<Person> guardians, Map<String, String> addresses, bool isRead, bool isArchived}) {
    return Registration(
      id ?? this.id,
      type ?? this.type,
      student ?? this.student,
      guardians: guardians ?? this.guardians,
      addresses: addresses ?? this.addresses,
      isRead: isRead ?? this.isRead,
      isArchived: isArchived ?? this.isArchived,
    );
  }

  @override
  String toString() {
    return 'Registration{id: $id, type: $type, student: $student, guardians: $guardians, address: $addresses, isRead: $isRead, isArchived: $isArchived}';
  }
}

I change the Registration model to use final Map<dynamic, dyanmic> addresses; to try to work around it, and then I get this error:

E/flutter (26131): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: type 'List<Object>' is not a subtype of type 'List<Person>'
E/flutter (26131): null

I can't really work around that one. Here is the json from final json = jsonEncode(data);

{"address":"555 Blah Blah Ct, Charleston, TX 55555","student":{"firstName":"Scott","lastName":"Pierce","phone":"5555555555","email":"test@gmail.com"},"isArchived":false,"isRead":true,"guardians":[{"firstName":"Jarjar","lastName":"Binks","phone":"5555555555","email":"test2@gmail.com"}],"type":"Moo moo","id":"VqUt06fi9IekRSag0DdR"}
k-paxian commented 4 years ago
type 'List<Object>' is not a subtype of type 'List<Person>'

For the list issue you'll find an example at https://github.com/k-paxian/dart-json-mapper#iterable-types

k-paxian commented 4 years ago

You should try this approach

      JsonMapper.registerValueDecorator<List<Person>>(
          (value) => value.cast<Person>());
      JsonMapper.registerValueDecorator<Map<String, String>>(
          (value) => value.cast<String, String>());

// below you could try to serialize/deserialize stuff

class FirebaseMapper {
  static T fromDocument<T>(DocumentSnapshot document) {
    // as far as I know 'document.data' is already Map<String, dynamic>, so
    return JsonMapper.fromMap<T>(data);
  }
  FirebaseMapper._();
}

and let me know the result please.

And for the copy, you could use JsonMapper.clone() someday.

ScottPierce commented 4 years ago

You are right that document.data is Map<String, dynamic>, the problem seems to be coming from internal Maps within that.

I'll try that when I can revert back to this library to give it a shot, and let you know the results.

Out of curiosity (again I'm new to Dart), what are the limitations of dart that this value decorator approach is necessary?

k-paxian commented 4 years ago

It's all about speed and performance. The point is that at the moment it's not possible to dynamically instantiate an iterable of a certain strong type. Just because iterable types has to be defined in compile time, so compiler will have a chance to optimize them, not sure about all the tiny details, but the general direction is correct.

At runtime you are allowed to use cast mechanism only. But to use this approach you should hardcode the exact iterable type somewhere in the code anyway.

I don't like this approach as well, maybe in future I'll be able to transfer this responsibility to generated code.

More info and examples you could find here https://github.com/k-paxian/dart-json-mapper#iterable-types

k-paxian commented 4 years ago

Feel free to re-open, when you are ready to continue.