23

I'm wondering how can I parse a nested json to a class with generic types. My intention is to wrap responses from the backend (like loginRespose that contains a token) with a code and a message

I have

class BaseResponse<T>{
  int code;
  String message;
  T responseObject;

  BaseResponse.fromJson(Map<String, dynamic> parsedJson)
    : code = parsedJson['Code'],
      message = parsedJson['Message'],
      responseObject = T.fromJson(parsedJson['ResponseObject']); //This is what I'd like to do
}

Obviously the last line throws an error because T doesn't has a named constructor "fromJson". I tried adding some restrictions to the Type but I didn't find any solutions. Do you have any idea on how to pull this off?

Sebastian
  • 3,666
  • 2
  • 19
  • 32
  • 1
    I don't think that is possible. There's no interface for constructors and flutter disabled `dart:mirror`. Maybe store inside `responseObject` the json object directly. And parse it individually. Or you could pass a custom deserializer to `fromJson` ctor – Rémi Rousselet Jul 28 '18 at 21:40
  • 1
    @RémiRousselet Thank you for your answer. Would you mind on exemplifying some of those solutions? Thank you very much – Sebastian Jul 28 '18 at 21:46
  • Found a better solution. Here it is :) – Rémi Rousselet Jul 28 '18 at 22:20
  • You might want to look at built_value package. It promises "any object model that you can design can be serialized, including full use of generics and interfaces. Some other libraries require concrete types or do not fully support generics." – Carson Holzheimer Aug 16 '18 at 06:19
  • @CarsonHolzheimer the biggest problem here is not serialization, but deserialization. – Rémi Rousselet Aug 16 '18 at 08:04
  • @RémiRousselet built_value equally supports deserialization. It's not straightforward but it is possible to do what Sebastian requires. – Carson Holzheimer Aug 17 '18 at 06:55

3 Answers3

14

You can't do such thing, at least not in flutter. As dart:mirror is disabled and there's no interface for classes constructors.

You'll have to take a different route.

I'll recommend using POO instead. You would here give up on deserializing responseObject from your BaseResponse. And then have subclass of BaseResponse handles this deserialization

Typically you'd have one subclass per type:

class IntResponse extends BaseResponse<int> {
  IntResponse.fromJson(Map<String, dynamic> json) : super._fromJson(json) {
    this.responseObject = int.parse(json["Hello"]);
  }
}

You can then hide this mess away by adding a custom factory constructor on BaseResponse to make it more convenient to use.

class BaseResponse<T> {
  int code;
  String message;
  T responseObject;

  BaseResponse._fromJson(Map<String, dynamic> parsedJson)
      : code = parsedJson['Code'],
        message = parsedJson['Message'];

  factory BaseResponse.fromJson(Map<String, dynamic> json) {
    if (T == int) {
      return IntResponse.fromJson(json) as BaseResponse<T>;
    }
    throw UnimplementedError();
  }
}

Then either instantiate the wanted type directly, or use the factory constructor :

final BaseResponse foo = BaseResponse.fromJson<int>({"Hello": "42", "Code": 42, "Message": "World"});
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
5

You can achieve this with the built_value package (you'll also need built_value_generator and build_runner). Your class will look something like this:

part 'base_response.g.dart';

abstract class BaseResponse<T> implements Built<BaseResponse<T>, BaseResponseBuilder<T>> {
  int code;
  String message;
  T responseObject;

  factory BaseResponse([updates(BaseResponseBuilder<T> b)]) = _$BaseResponse<T>;

  static Serializer<BaseResponse> get serializer => _$baseResponseSerializer;
}

You will have to run flutter packages pub run build_runner build to make the generated file. Then you use it like this:

BaseResponse baseResponse = serializers.deserialize(
  json.decode(response.body),
  specifiedType: const FullType(BaseResponse, const [FullType(ConcreteTypeGoesHere)])
);

There's just one more bit of boilerplate you have to take care of. You need another file called serializers.dart. You need to manually add all the classes you want to deserialize here, and also an addBuilderFactory function for each class that takes a type parameter - and for each concrete type you want to use.

part 'serializers.g.dart';

@SerializersFor(const [
  BaseResponse,
  ConcreteTypeGoesHere,
])
final Serializers serializers = (_$serializers.toBuilder()
      ..addBuilderFactory(
        FullType(BaseResponse, const [const FullType(ConcreteTypeGoesHere)]),
        () => new BaseResponseBuilder<ConcreteTypeGoesHere>()
      )
      ..addPlugin(StandardJsonPlugin()))
    .build();

Then re-run flutter packages pub run build_runner build

Makes me wish for Gson... :S

Carson Holzheimer
  • 2,890
  • 25
  • 36
0

Here is my approach:

class Wrapper<T, K> {
  bool? isSuccess;
  T? data;

  Wrapper({
    this.isSuccess,
    this.data,
  });

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

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

Wrapper<T, K> _$WrapperFromJson<T, K>(Map<String, dynamic> json) {
  return Wrapper<T, K>(
    isSuccess: json['isSuccess'] as bool?,
    data: json['data'] == null ? null : Generic.fromJson<T, K>(json['data']),
  );
}

class Generic {
  /// If T is a List, K is the subtype of the list.
  static T fromJson<T, K>(dynamic json) {
    if (json is Iterable) {
      return _fromJsonList<K>(json) as T;
    } else if (T == LoginDetails) {
      return LoginDetails.fromJson(json) as T;
    } else if (T == UserDetails) {
      return UserDetails.fromJson(json) as T;
    } else if (T == Message) {
      return Message.fromJson(json) as T;
    } else if (T == bool || T == String || T == int || T == double) { // primitives
      return json;
  } else {
      throw Exception("Unknown class");
    }
  }

  static List<K> _fromJsonList<K>(List<dynamic> jsonList) {
    return jsonList?.map<K>((dynamic json) => fromJson<K, void>(json))?.toList();
  }
}

In order to add support for a new data model, simply add it to Generic.fromJson:

else if (T == NewDataModel) {
  return NewDataModel.fromJson(json) as T;
}

This works with either generic objects:

Wrapper<Message, void>.fromJson(someJson)

Or lists of generic objects:

Wrapper<List<Message>, Message>.fromJson(someJson)
Ovidiu
  • 8,204
  • 34
  • 45