HTTP requests and JSON models

Flutter · Practical · 14 min

Nearly every app talks to a backend, and typed models catch API mismatches early.

Add the official `http` package (flutter pub add http) and call http.get(Uri.parse(url)), http.post(uri, headers: ..., body: jsonEncode(data)). Each returns a Future<Response> with statusCode and body. Check the status yourself; http doesn't throw on 404 or 500. Network failures throw (e.g. SocketException, or ClientException).

Decode with jsonDecode(response.body) from dart:convert. The result is untyped (Map<String, dynamic> or List<dynamic>), so convert it to model classes at the boundary:

  • A factory User.fromJson(Map<String, dynamic> json) constructor, ideally using Dart 3 map patterns to check types: {'id': int id, 'name': String name}.
  • A toJson() method for sending.

For larger models, code generation with json_serializable (plus build_runner) or freezed writes this boilerplate for you.

Good practices:

  • Keep HTTP code out of widgets, in a repository or service class that returns models and throws domain errors.
  • Use a single http.Client and close it when done; it reuses connections.
  • Add timeouts: .timeout(const Duration(seconds: 10)).
  • Decode very large JSON off the UI thread with compute or Isolate.run.
  • On Android, release builds need the INTERNET permission in the manifest; macOS apps need the network client entitlement.

dio is a popular alternative with interceptors, cancellation and progress callbacks.

Example

import 'dart:convert';
import 'package:http/http.dart' as http;

class Todo {
  const Todo({required this.id, required this.title, required this.done});
  final int id;
  final String title;
  final bool done;

  factory Todo.fromJson(Map<String, dynamic> json) => switch (json) {
        {'id': int id, 'title': String title, 'completed': bool done} =>
          Todo(id: id, title: title, done: done),
        _ => throw FormatException('Bad todo JSON: $json'),
      };
}

Future<List<Todo>> fetchTodos(http.Client client) async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/todos?_limit=5');
  final res = await client.get(uri).timeout(const Duration(seconds: 10));
  if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
  final list = jsonDecode(res.body) as List<dynamic>;
  return [for (final item in list) Todo.fromJson(item as Map<String, dynamic>)];
}

Remember

  • Check statusCode yourself — http doesn't throw on 4xx/5xx
  • Convert JSON to typed models at the boundary, e.g. with map patterns
  • Keep HTTP in a service/repository, not in widgets

Watch out for

  • Using json['field'] dynamically throughout the UI, so API changes crash at run time far from the cause.

Check yourself

  1. A server returns status 500. What does `await http.get(uri)` do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/cookbook/networking/fetch-data