Short answer
Print response.body and look at the first character. If it is {, the top level is a map: read the list from its key, for example (json['data'] as List). Check the status code before parsing, because error responses are usually objects. Then map each item with fromJson.
Why the JSON isn’t the list you expected, and the fix
01
The list is wrapped in an object
“Unfortunately, jsonDecode() returns a dynamic, meaning that you do not know the types of the values until runtime.” Flutter, JSON and serialization
jsonDecode gives you whatever shape the server sent. Many APIs send {"data": [...], "page": 1}, which decodes to a Map; take the list out of it, then convert each item.
final json = jsonDecode(response.body) as Map<String, dynamic>; final items = (json['data'] as List) .map((e) => Product.fromJson(e as Map<String, dynamic>)) .toList();02
The server returned an error object
“If the server does not return an OK response with a status code of 200, then throw an exception.” Flutter, Fetch data from the internet
A 401, 404 or 500 usually comes back as {"error": "..."} even when the success response is a list. Check the status code first, so you parse only the shape you expect.
if (response.statusCode != 200) { throw Exception('Request failed: ${response.statusCode} ${response.body}'); } final list = jsonDecode(response.body) as List;03
A single item where the model expects a list
An endpoint such as /products/42 returns one object. Use fromJson on the map directly instead of mapping over it, or call the list endpoint.
final product = Product.fromJson( jsonDecode(response.body) as Map<String, dynamic>, );
Checked against the vendor’s docs on : Flutter: JSON and serialization; Flutter cookbook: Fetch data from the internet.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
Related: Flutter “type Null is not a subtype of type String” · Flutter “SocketException: Failed host lookup” · Flutter “Bad state: No element” · Flutter “Looking up a deactivated widget’s ancestor is unsafe”