Skip to main content

JSON Adapter

PRO
Pro preview

This feature is part of FD Components Pro, which is documented as a preview and is not publicly available yet. See Editions and Availability.

FdcJsonAdapter connects an FdcDataSet to a backend through application-owned callbacks. It defines the data contract but does not choose HTTP, WebSocket, local IPC, an SDK client, or any other transport.

Use it when the application already has a service layer or when requests do not map naturally to HTTP endpoints. For modeled HTTP requests, use REST Adapter.

Minimal read-only adapter

final adapter = FdcJsonAdapter(
source: 'customers',
onOpen: (event) async {
final response = await customerService.query(event.toMap());
return response;
},
);

final customers = FdcDataSet(
fields: const [
FdcIntegerField(name: 'customer_id', isKey: true),
FdcStringField(name: 'company', size: 120),
FdcStringField(name: 'city', size: 80),
],
adapter: adapter,
);

await customers.open();

Without onApply, the adapter reports itself as read-only. The open handler receives an immutable FdcJsonOpenEvent and must return the backend's final response envelope.

Complete adapter

final adapter = FdcJsonAdapter(
source: 'orders',
openTimeout: const Duration(seconds: 30),
applyTimeout: const Duration(seconds: 20),
metaBuilder: (operation) => <String, Object?>{
'tenant': currentTenantId,
'requestId': createRequestId(),
'operation': operation.name,
},
onOpen: (event) => orderService.query(event.toMap()),
onApply: (event) => orderService.apply(event.toMap()),
onAggregate: (event) => orderService.aggregate(event.toMap()),
);

The three handlers have distinct responsibilities:

HandlerEventResponsibility
onOpenFdcJsonOpenEventExecute filtering, sorting, search, selection and paging, then return final rows.
onApplyFdcJsonApplyEventPersist the complete delete/update/insert changeset and return operation success or errors.
onAggregateFdcJsonAggregateEventCalculate aggregates over the complete matching query, without page slicing.

Each handler may return a JSON-compatible Dart object, JSON text, or UTF-8 JSON bytes. Handler exceptions flow through the normal adapter and dataset error pipeline.

The backend is authoritative

FdcJsonAdapter advertises backend capabilities for filtering, sorting, paging, total count, search, and selected-key filtering. Aggregate capability is enabled only when onAggregate is supplied.

The backend must execute every operation represented by the request. Returned rows are treated as the final result; the adapter does not fetch an unfiltered document and repeat remote filtering or paging locally.

onOpen: (event) async {
final request = event.request;

return repository.queryCustomers(
filters: request.filters,
sorts: request.sorts,
search: request.search,
offset: request.offset,
limit: request.limit,
includeTotalCount: request.includeTotalCount,
);
},

See Server-Side Operations for capability and query semantics.

Event serialization

Every event exposes both toMap() and toJson():

onOpen: (event) async {
final jsonText = event.toJson();
final responseBytes = await transport.send(jsonText);
return responseBytes;
},

Use event.request or event.changeSet when the service layer accepts FDC objects directly. Use toMap() or toJson() when forwarding the canonical backend contract.

The complete property-level specification is documented in JSON Contract.

Writable datasets

Supplying onApply enables insert, update, and delete persistence:

final adapter = FdcJsonAdapter(
source: 'customers',
onOpen: loadCustomers,
onApply: (event) async {
await database.transaction((transaction) async {
for (final action in event.actions) {
switch (action.action) {
case FdcJsonApplyActionType.delete:
await transaction.deleteCustomer(action.keys);
break;
case FdcJsonApplyActionType.update:
await transaction.updateCustomer(action.keys, action.values);
break;
case FdcJsonApplyActionType.insert:
await transaction.insertCustomer(action.recordId, action.values);
break;
}
}
});

return <String, Object?>{
'source': event.source,
'success': true,
};
},
);

Writable datasets need key fields so update and delete actions can carry stable original key values. Actions are emitted in deterministic order: delete, update, then insert.

When the backend generates IDs, timestamps, or normalized values, return them with the matching recordId. The adapter writes those authoritative values back to the corresponding local record.

Aggregates

Supplying onAggregate enables adapter-side aggregate requests:

onAggregate: (event) async {
final result = await reportingService.aggregate(event.toMap());
return <String, Object?>{
'source': event.source,
'aggregates': result,
};
},

Aggregate requests include filters, search, selected keys, and requested aggregate definitions. They intentionally omit offset and limit, so results describe the complete matching query rather than the visible page.

Decimal sum and avg responses are materialized as FdcDecimal.

Metadata

Use metaBuilder for request context that can vary by operation:

metaBuilder: (operation) async => <String, Object?>{
'tenant': currentTenantId,
'locale': currentLocale.toLanguageTag(),
'requestId': await requestIds.next(),
},

The builder receives FdcJsonOperation.open, apply, or aggregate. Returning null or an empty map omits the meta property. Metadata is application-defined; FDC transports it but does not interpret it.

Timeouts

openTimeout covers open and aggregate handlers. applyTimeout covers apply handlers. Both default to 60 seconds and include asynchronous metadata construction.

FdcJsonAdapter(
source: 'orders',
openTimeout: const Duration(seconds: 30),
applyTimeout: Duration.zero,
onOpen: loadOrders,
onApply: applyOrders,
)

Duration.zero disables the corresponding timeout. Negative durations are rejected. Timeout failures become FdcDataAdapterException instances with code timeout.

Type materialization

Response properties are matched to dataset fields case-insensitively and stored under the schema's canonical field name. A row containing duplicate names that differ only by case is rejected as ambiguous.

FDC materializes typed values using field metadata, including:

  • decimals from JSON numbers or decimal strings;
  • dates and date-times from ISO-compatible strings;
  • times from FdcTime strings;
  • GUIDs from string values;
  • integers and booleans from compatible JSON scalar values.

Keep backend JSON values aligned with the field type. Do not use binary floating-point serialization when exact decimal text is available.

Error handling

The adapter rejects malformed JSON, bare row arrays, missing fixed envelope properties, mismatched source, invalid totalCount, invalid apply correlation, and non-JSON response values.

try {
await customers.open();
} on FdcDataAdapterException catch (error) {
logAdapterFailure(error.operation, error.code, error.message);
}

Contract-shape violations can also surface as StateError or ArgumentError, because they represent an invalid backend response rather than a normal business validation failure. Validate the backend against JSON Contract before mapping messages into end-user UI.