6

Context

In my code, I have an interface called AbstactDataModel that is used as a starting point for all the data model classes. This is implemented so that i know that whatever xyzModel class I may need, it will have a fromJson(Map<String, dynamic> json) function that will return a new instance of the class built using the given json and a getter for the type.

// In abstract_model.dart
abstract class AbstractDataModel {
  ///
  /// returns a String containing the class name
  /// For example, the class ColumnModel will return 'column'
  ///
  String get type;

  ///
  /// Will call the [.fromJson] constructor and return a new instance of the
  /// object
  ///
  dynamic fromJson(Map<String, dynamic> json);
}

And here is an example of a DataModel class that extends the AbstractDataModel:

// in group_model.dart
class GroupModel extends AbstractDataModel {
  int _id;
  String _name;

  // Constructors
  GroupModel.empty();
  GroupModel.fromJson(Map<String, dynamic> json) {
    _id = parseToInt(json['id'].toString());
    _name = parseToString(json['name'].toString());
  }

  // Getters for private fields
  int get id => _id;
  String get name => _name;
  @override
  String get type => 'group';

  @override
  GroupModel fromJson(Map<String, dynamic> json) => GroupModel.fromJson(json);
}

'Broken' code

I made a function that sends an API request to a server, and then would parse the json response into a DataModel type of object. In order to avoid code duplication and make the source code easier to maintain, i wanted to create a generic function, like so:

// inside the API class (which is a singleton)
Future<T> getObject<T extends AbstractDataModel>({
@required String command,
@required Map<String, dynamic> params,
}) async {
    final Response response = await _sendRequest(
        command: command,
        params: params,
    );
    T object;
    final Map<String, dynamic> body =
        jsonDecode(response.body)['result'] as Map<String, dynamic>;
    if (body == null) {
        print('Request failed.');
        throw const Failure('Failed request to fetch object.');
    } else {
        object = (T as AbstractDataModel).fromJson(body) as T;
        print('Successfully fetched ${object.type}');
        return object;
    }
}

And the way I expected to use this, is as follows:

GroupModel group = await API().getObject<GroupModel>();

Expected behaviour

I expect the getObject function to know that, since T extends AbstractDataModel, then the fromJson(...) function exists, and use it to create a new GroupModel object, all while only accepting objects that extend the AbstractDataModel, i.e. throwing an error for getObject<int>() saying something like 'int does not extend AbstractDataModel`.

Actual behaviour

During execution, I get an unhandled error that says the type _Type is not a subtype of AbstractDataModel and as such, the cast (T as AbstractDataModel) is invalid.

Also, setting a breakpoint and checking some values via the debugger, i tested the following statements:

T is AbstractDataModel: false
T is GroupModel: true
T is AnyOtherDataModelThatExtendsAbstractDataModel: true

So I wouldn't even be able to handle it via a switch statement, since even if i call getObject<GroupModel>(), the statement T is ProjectModel for example, is still evaluated as being true. Any pointers on what i am doing wrong?

Mike
  • 135
  • 1
  • 9
  • Can you try to replace `object = (T as AbstractDataModel).fromJson(body) as T;` with `object = (T as T).fromJson(body);` ? – Augustin R Sep 07 '20 at 15:46
  • `T` is a `Type`, not an instance of that type. It does not make sense to cast a `Type` object. Similarly, [`is` makes sense only when the left operand is an instance](https://stackoverflow.com/a/61584189/179715). – jamesdlin Sep 07 '20 at 19:16
  • I don't understand what `(T as AbstractDataModel).fromJson` is trying to do, especially since `fromJson` isn't `static`. Why can't you just call `object.fromJson`? – jamesdlin Sep 07 '20 at 19:23
  • @jamesdlin I am trying to call the `fromJson` function as if it was a constructor, similarly to how i would call `GroupModel group = GroupModel.fromJson()`. If i call `object.fromJson` then I get an error because `object` is `null`. The reason I was casting the type `T` as an `AbstractDataModel` was because I was trying to "make it look" like a constructor, i guess. – Mike Sep 08 '20 at 07:09
  • To be called on a *class* instead of an object instance, `fromJson` would need to be `static`. But unless you have an object instance, there is no way for virtual dispatch to work. – jamesdlin Sep 08 '20 at 07:22
  • Also see https://stackoverflow.com/q/56135525/. – jamesdlin Sep 08 '20 at 07:28
  • Alternatively you could add, say, `class AbstractDataModelFactory { AbstractDataModel fromJson(Map json); }`, `class GroupModelFactory { @override GroupModel fromJson(Map json) { ... } }` and then pass an instance of the appropriate factory to `getObject`. – jamesdlin Sep 08 '20 at 07:32
  • I have the same question ... with 20 methods that copy/paste the same code... Don't manage to write a generic method... – Vincent Ricosti Jun 20 '22 at 14:03

1 Answers1

0

You can use this package in order to use fromJson in a generic class Json_Mapper

JsonMapper.register(JsonObjectMapper(
  (CustomJsonMapper mapper, Map<String, dynamic> json) =>
      Model.fromJson(json),
  (CustomJsonMapper mapper, Model instance) => <String, dynamic>{},
));

and then in your Generic class

T t = JsonMapper.deserializeFromMap<T>(json)!