DevTools

Data Serialization

How cached query data is turned into the JSON tree shown in the overlay, and how to make custom model types readable.

The overlay shows cached query data as an expandable JSON tree. How readable that tree is depends on the type T returned by the fetcher: most models need no configuration, and the rest need one option.


Serialization resolution order

QoraClient has no knowledge of domain types. When a fetch settles, the result is passed to the active tracker using the following resolution order:

PriorityConditionResult
1QoraOptions.toJson is setCalls the provided function and uses its return value
2String, num, bool, or nullPassed through unchanged
3ListEach element is recursively serialized
4MapEach value is recursively serialized
5Object with a toJson() methodCalled via dynamic dispatch, result used
6Everything else.toString() called, displayed as a plain string

The walk is recursive. A List<Map<String, User>> is handled correctly without configuration, provided User implements toJson().


Common scenarios

json_serializable and freezed

Both code generators produce a toJson() method. The serializer detects and calls it automatically. No additional configuration is required.

user_repository.dart
// @JsonSerializable() or @freezed: no extra config needed
fetchQuery<User>(
  key: ['users', userId],
  fetcher: () => api.getUser(userId),
);

The overlay renders the full object graph as an expandable tree.


Raw Map responses

When the fetcher returns Map<String, dynamic> directly (for example, after manually decoding the HTTP response body), no configuration is required.

user_repository.dart
fetchQuery<Map<String, dynamic>>(
  key: ['users', userId],
  fetcher: () async {
    final response = await http.get(Uri.parse('/users/$userId'));
    return jsonDecode(response.body) as Map<String, dynamic>;
  },
);

This approach requires no serialization configuration. The deserialization step must be handled in the fetcher or repository layer rather than in a consuming widget.


Manual models without toJson

When a model has no toJson() method, the serializer falls back to .toString() and displays a single string node. To produce structured output, supply an explicit serializer via QoraOptions.toJson.

user_repository.dart
fetchQuery<User>(
  key: ['users', userId],
  fetcher: () => api.getUser(userId),
  options: QoraOptions(
    toJson: (data) => (data as User).toJson(),
  ),
);

Lists

Lists of typed objects follow the same resolution rules. When the item type implements toJson(), each element is serialized automatically.

posts_repository.dart
fetchQuery<List<Post>>(
  key: ['posts'],
  fetcher: () => api.getPosts(),
  // Post implements toJson(): auto-detected, no extra config.
);

When individual items lack toJson(), provide a serializer:

posts_repository.dart
fetchQuery<List<Post>>(
  key: ['posts'],
  fetcher: () => api.getPosts(),
  options: QoraOptions(
    toJson: (data) => (data as List<Post>).map((p) => p.toJson()).toList(),
  ),
);

The toJson option

QoraOptions.toJson accepts any function with the signature:

Object? Function(Object? data)

The return value must be JSON-safe: Map<String, dynamic>, List, String, num, bool, or null. Returning any other type will cause a runtime error when the DevTools renderer encodes the value.

user_repository.dart
// The cast is safe: T is User and data is the fetcher result.
options: QoraOptions(
  toJson: (data) => (data as User).toJson(),
),

toJson follows the same merge semantics as all other QoraOptions fields. A value on QoraClientConfig.defaultOptions applies globally and can be overridden per query.


When to use each approach

SituationRecommendation
json_serializable or freezed modelsNo configuration required. Auto-detected.
Raw Map or List from jsonDecodeNo configuration required. Passed through.
Manual model with a toJson() methodNo configuration required. Auto-detected.
Model without toJson()Provide QoraOptions(toJson: ...).
Custom serialization with a non-standard toJson signatureProvide QoraOptions(toJson: ...).

Production overhead

The serialization path (_serializeForTracker) is only executed when QoraTracker.needsSerialization returns true. In production, QoraClient defaults to NoOpTracker, which returns false. No serialization runs and no overhead is introduced.

main.dart
final client = QoraClient(
  tracker: kDebugMode ? tracker : null, // null activates NoOpTracker
);
Copyright © 2026