Generic deserialization in Flutter / Dart

Viewed 688

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 Answers

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