Generic deserialization in Flutter / Dart

I'm trying to write a HTTP driver class that takes in a generic class and deserializes the response. I haven't found a good, clean way to do this in Flutter.

I've defined datamodel classes like this:

class MyClass {
  String field1;
  String field2;
  
  MyClass.fromJson(Map<dynamic, dynamic> json) 
    : field1 = json["field1"],
      field2 = json["field2"];
}

This works well and good if I do it manually...

MyClass makeRequest() {
   Response response = http.get(url);
   MyClass class = MyClass.fromJson(jsonDecode(response.body));
   return class;
}

What I want, is to make a generic HTTP driver like this:

void makeRequest<T>() {
   Response response = http.get(url);
   T parsed = T.fromJson(jsonDecode(response.body));
   return parsed;
}

Is there a way to do this in Flutter/Dart? I've been trying to figure out the right syntax to use a base class and extends but haven't gotten it. Any ideas?

1 answer

  • answered 2021-04-08 04:02 Bach

    This is what I usually use in my network call, feel free to use. Btw, I recommend the dio package for convenient headers and params config, as well as other error handling features.

    // Define an extension
    extension BaseModel on Type {
      fromJson(Map<String, dynamic> data) {}
    }
    
    // For single object
    Future<T> makeGetRequest<T>({String url, Map<String, dynamic> params}) {
      return http
          .get(buildUrl(url, params)) // Don't need the buildUrl() if you use Dio
          .then((response) => handleJsonResponse(response))
          .then((data) => T.fromJson(data));
    
    // For list of object
    Future<List<T>> makeGetRequestForList<T>({String url, Map<String, dynamic> params}) {
      return http
          .get(buildUrl(url, params)) // Don't need the buildUrl() if you use Dio
          .then((response) => handleJsonResponse(response))
          .then((data) => List<T>.from(data.map((item) => T.fromJson(item)));
    }
    
    // Helper classes without Dio
    String buildUrl(String url, [Map parameters]) {
      final stringBuilder = StringBuffer(url);
      if (parameters?.isNotEmpty == true) {
        stringBuilder.write('?');
        parameters.forEach((key, value) => stringBuilder.write('$key=$value&'));
      }
      final result = stringBuilder.toString();
      print(result);
      return result;
    }
    
    // With Dio, you can simply do this:
    final res = await API().dio
          .get(url, queryParameters: params) // Don't need the [buildUrl] here      
          .then((response) => handleJsonResponse(response))
          .then((data) => T.fromJson(data));
    
    // Handle JSON response
    handleJsonResponse(http.Response response, [String endpoint = '']) {
      print(
          'API: $endpoint \nCODE: ${response.statusCode} \nBODY: ${response.body}');
      if (_okStatus.contains(response.statusCode)) {
        return jsonDecode(response.body);
      }
      if (response.statusCode == HttpStatus.unauthorized) {
        throw Exception(response.statusCode);
      } else {
        throw Exception("HTTP: ${response.statusCode} ${response.body}");
      }
    }
    

    Usage:

    // Example class
    class Post {
      final String title;
    
      Post({this.title});
    
      @override
      Post.fromJson(Map<String, dynamic> data) : title = data['title'];
    }
    
    // Use the function
    Future<Post> getPost() async {
      final result = await makeGetRequest<Post>(params: {'post_id': 1});
      return result;
    }