An API may be well documented and still return something your application did not expect. A proxy can return HTML, an optional field can disappear, and a numeric identifier can change format. A reliable client validates the boundary before displaying a result.

Decode once, validate once

Flutter supports manual JSON conversion and generated serialization. Both approaches work better when conversion belongs to a model or mapper rather than being repeated in widgets. Generated code reduces repetitive field mapping, but domain rules still need explicit decisions and tests.

class ArticleSummary {
  const ArticleSummary(this.id, this.title);
  final int id;
  final String title;

  factory ArticleSummary.fromJson(Map<String, dynamic> json) {
    final id = json['id'];
    final title = json['title'];
    if (id is! int || title is! String || title.trim().isEmpty) {
      throw const FormatException('Invalid article summary');
    }
    return ArticleSummary(id, title.trim());
  }
}

This standalone model accepts a narrow contract. If your backend deliberately uses string identifiers, model that contract instead of adding undocumented conversions until every input appears to work. Unknown extra fields can usually be ignored while required fields remain strict.

Separate transport and product decisions

A service can request data and inspect the response status. A repository can decide whether to use cached data and translate expected failures into something the feature understands. A widget should not need to know whether the failure came from JSON decoding or an expired network connection to offer a useful retry.

Avoid retry surprises

Retrying a catalog read is different from retrying an order submission. Before automatically repeating a write, agree on duplicate prevention with the backend. A disabled submit button helps user experience, but it cannot prove that the server did not already process a request whose response was lost.

Use a compact contract test set

  • A normal response and a successful empty response.
  • Missing, null, and incorrectly typed required fields.
  • An unexpected status code and a non-JSON body.
  • A timeout followed by a successful retry.
  • A duplicate response item across pagination boundaries.

Keep sanitized response fixtures in tests. They make backend changes reviewable without depending on a live service, and they avoid accidentally committing personal data from production logs.

Official references: Flutter JSON and serialization and Fetching data in Flutter.