Data Serialization
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:
| Priority | Condition | Result |
|---|---|---|
| 1 | QoraOptions.toJson is set | Calls the provided function and uses its return value |
| 2 | String, num, bool, or null | Passed through unchanged |
| 3 | List | Each element is recursively serialized |
| 4 | Map | Each value is recursively serialized |
| 5 | Object with a toJson() method | Called via dynamic dispatch, result used |
| 6 | Everything 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.
// @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.
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.
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.
fetchQuery<List<Post>>(
key: ['posts'],
fetcher: () => api.getPosts(),
// Post implements toJson(): auto-detected, no extra config.
);
When individual items lack toJson(), provide a serializer:
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.
// 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
| Situation | Recommendation |
|---|---|
json_serializable or freezed models | No configuration required. Auto-detected. |
Raw Map or List from jsonDecode | No configuration required. Passed through. |
Manual model with a toJson() method | No configuration required. Auto-detected. |
Model without toJson() | Provide QoraOptions(toJson: ...). |
Custom serialization with a non-standard toJson signature | Provide 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.
final client = QoraClient(
tracker: kDebugMode ? tracker : null, // null activates NoOpTracker
);