Skip to main content

JSON Contract

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.

This page specifies the canonical envelope used by FdcJsonAdapter and FdcRestAdapter. The contract is transport-independent: JSON may travel through HTTP, another service protocol, local IPC, or an application-owned client SDK.

The current contract is envelope-only. A bare array of rows is not a valid response.

Common rules

Every request and response contains a non-empty source string. A response whose source does not exactly match the configured adapter source is rejected.

Requests can include an application-defined meta object. FDC transports metadata but does not interpret it. Responses do not need to echo meta.

All JSON objects must use string keys. Values must be valid JSON values; non-finite numbers and application-specific Dart objects are rejected unless converted to transport values first.

Transport values

toMap() and toJson() convert typed values into JSON-compatible forms:

FDC/Dart valueJSON representation
null, String, bool, integer, finite numberCorresponding JSON scalar
FdcDecimalExact decimal string
DateTimeISO 8601 string; date fields use YYYY-MM-DD in apply values
FdcTimeTime string
FdcGuidGUID string
ListRecursively converted JSON array
MapRecursively converted JSON object with string keys

Using decimal strings avoids precision loss in backends whose JSON number type is binary floating point.

Open request

FdcJsonOpenEvent.toMap() creates the open envelope:

{
"source": "customers",
"meta": {
"tenant": "north-america",
"requestId": "req-1042"
},
"filters": [
{
"fieldName": "status",
"operator": "equals",
"value": "active"
}
],
"sorts": [
{
"fieldName": "company",
"sortType": "ascending"
}
],
"search": {
"text": "north",
"mode": "contains",
"caseSensitive": false,
"fields": ["company", "city"]
},
"offset": 0,
"limit": 100,
"selectedKeys": [
{"customer_id": 17}
],
"includeFields": true,
"includeTotalCount": true
}

Open request properties

PropertyRequiredMeaning
sourceYesLogical adapter source.
metaNoApplication-defined request metadata.
filtersYesOrdered effective filters; may be empty.
sortsYesOrdered effective sorts; may be empty.
searchNoPresent only when search is active.
offsetNoZero-based page offset when paging is active.
limitNoMaximum rows requested for the page.
selectedKeysNoPresent when selected-key-only filtering is active, including an empty array when no keys are selected.
includeFieldsNoPresent as true when field metadata is requested.
includeTotalCountYesWhether the backend must return the complete matching count.

Each filter contains fieldName, the FDC operator name, and value. caseSensitive: true is included only when requested. Sort priority follows array order.

The backend must execute the complete request before returning rows. Page slicing occurs after filtering, search, and sorting. totalCount describes the complete matching result before page slicing.

Open response

{
"source": "customers",
"rows": [
{
"customer_id": 17,
"company": "Northstar Trading",
"city": "Boston",
"credit_limit": "25000.00",
"registered_at": "2026-01-15"
}
],
"totalCount": 1248
}

Open response rules:

  • the root must be a JSON object;
  • source is required, non-empty, and must match the adapter source;
  • rows is required and must be an array of JSON objects;
  • totalCount is required when includeTotalCount is true;
  • totalCount, when present, must be an integer;
  • a bare rows array is invalid.

Response row properties are matched to dataset fields case-insensitively. The materialized row uses the field name declared in the dataset schema. A single response row must not contain duplicate keys that differ only by case, such as both Customer_ID and customer_id.

Extra response properties that do not correspond to requested dataset fields are ignored when field metadata is available. Missing fields materialize as null during a normal open.

Apply request

FdcJsonApplyEvent.toMap() emits one operation-level envelope:

{
"source": "customers",
"meta": {
"tenant": "north-america",
"requestId": "req-1043"
},
"actions": [
{
"action": "delete",
"recordId": 17,
"keys": {"customer_id": 41}
},
{
"action": "update",
"recordId": 23,
"keys": {"customer_id": 52},
"values": {"city": "Seattle"}
},
{
"action": "insert",
"recordId": 31,
"values": {
"company": "Harbor Systems",
"city": "Portland"
}
}
]
}

Actions are always ordered:

delete → update → insert

Apply action properties

PropertyDeleteUpdateInsert
actiondeleteupdateinsert
recordIdRequiredRequiredRequired
keysOriginal key valuesOriginal key valuesOmitted
valuesOmittedChanged values onlyInsert values

recordId is the dataset's internal correlation identifier. It is not a database primary key and must be returned unchanged when the backend supplies authoritative values for an insert or update.

Updates and deletes use original key values so a key field changed by the user still identifies the correct stored row.

Successful apply response

The minimal successful response is:

{
"source": "customers",
"success": true
}

Return optional authoritative rows when the backend generates or normalizes values:

{
"source": "customers",
"success": true,
"rows": [
{
"recordId": 31,
"values": {
"customer_id": 9104,
"company": "Harbor Systems LLC",
"updated_at": "2026-07-10T10:30:00Z"
}
}
]
}

Success response rules:

  • success must be the boolean true;
  • errors must not be present;
  • rows, when present, must be an array;
  • every row requires an integer recordId and an object values;
  • a recordId may reference an insert or update action, never a delete or unknown action;
  • duplicate response recordId values are rejected;
  • response row order is irrelevant.

totalCount is not part of an apply response.

Failed apply response

{
"source": "customers",
"success": false,
"errors": [
{
"recordId": 23,
"field": "city",
"code": "invalid_city",
"message": "The city value is not valid for this account."
}
]
}

Failure response rules:

  • success must be the boolean false;
  • errors is required and must be a non-empty array;
  • each error requires a non-empty message;
  • recordId is optional, but when present it must reference an action in the request;
  • field or fieldName can identify the affected field;
  • code can carry an application-specific machine-readable value.

An error without recordId becomes an operation-level apply error. The JSON apply contract is not a mixed partial-success protocol: one envelope reports either success or failure for the complete changeset.

Aggregate request

FdcJsonAggregateEvent.toMap() creates:

{
"source": "orders",
"filters": [
{
"fieldName": "status",
"operator": "equals",
"value": "open"
}
],
"search": {
"text": "north",
"mode": "contains",
"caseSensitive": false,
"fields": ["customer"]
},
"aggregates": [
{"fieldName": "total", "aggregate": "sum"},
{"fieldName": "total", "aggregate": "avg"},
{"fieldName": "order_id", "aggregate": "count"}
]
}

Aggregate requests may also contain meta and selectedKeys. They deliberately omit sorts, offset, and limit. The backend calculates against the complete matching filter/search/selection result.

Aggregate response

Nested response form:

{
"source": "orders",
"aggregates": {
"total": {
"sum": "154280.75",
"avg": "481.22"
},
"order_id": {
"count": 321
}
}
}

The adapter also accepts flat keys:

{
"source": "orders",
"aggregates": {
"total.sum": "154280.75",
"total:avg": "481.22",
"order_id.count": 321
}
}

The root and aggregates must both be objects. Decimal sum and avg values are materialized as FdcDecimal. Missing sum values become FdcDecimal.zero; other missing aggregate values remain null.

Backend implementation checklist

Before connecting a production backend, verify that it:

  • validates source and returns it in every response;
  • executes filters, sort priority, search, selected keys, and paging exactly once;
  • returns an integer totalCount whenever requested;
  • treats apply as one operation-level success or failure;
  • correlates authoritative apply values by FDC recordId;
  • calculates aggregates without applying page offset or limit;
  • serializes decimals without losing precision;
  • returns only valid JSON values and string object keys;
  • produces deterministic errors for malformed requests.