Validation and transformation
One of the best features of the FileMaker platform is how easily you can add, remove, and rename fields. That's also the risk for a Web Viewer app: if a field the app depends on gets renamed in FileMaker, nothing about the schema stops you from shipping code that silently reads undefined.
@adt/fmdapi closes that gap with runtime validation. Every layout client generated by adt typegen carries a schema, so a request that comes back from FileMaker with data the app doesn't expect throws immediately instead of quietly corrupting your UI.
Standard Schema, generated as Zod
DataApi accepts an optional schema option built on Standard Schema — any validation library that implements the spec will work. adt typegen generates Zod (v4) schemas by default, so that's what you'll see in a normal project.
Each layout gets a generated schema file with a consistent naming pattern:
-
Z<Schema>— the Zod object schema for a record'sfieldData. -
T<Schema>— the inferred TypeScript type,z.infer<typeof Z<Schema>>. -
If the layout has portals:
Z<Schema>Portals, an object mapping each portal name to its own Zod schema, andT<Schema>Portals, its inferred type.
The generated client wires these into the schema option automatically:
export const client = DataApi({
adapter: new WebViewerAdapter({ scriptName: "ADT_execute_data_api" }),
layout: "Contacts",
schema: { fieldData: ZContacts, portalData: ZContactsPortals },
});
You'll rarely write a schema option by hand because Typegen maintains it. See Generate types with Typegen for the generation workflow and adt.config.json for the configuration format.
Note Turning validation off Set "validator": false at the top level of the adt.config.json config block — alongside "path", "clientSuffix", and "layouts", not inside an individual layout entry — to generate TypeScript types only, with no runtime schema and no Z<Schema> output for every layout in that block. Every other setting ("zod", "zod/v4", "zod/v3") generates Zod schemas; "zod/v4" is the default.
What gets validated, and when
Validation runs on any client method that returns fieldData from FileMaker: list, listAll, get, find, findOne, findFirst, maybeFindFirst, and findAll. Each record's fieldData is validated (and transformed, if the schema coerces types) against Z<Schema>; each portal row is validated against the matching entry in Z<Schema>Portals if one exists.
create, update, and delete do not run schema validation — they only return a recordId/modId, not field data, so there's nothing to validate.
Handling validation failures
If any record's fieldData or portal row fails validation, the client:
-
Logs a
console.errornaming the layout and listing every validation issue. -
Throws an error — no data is returned to your app, even though FileMaker responded with a normal, successful result.
That means a validation failure surfaces as a JavaScript exception on the call site, not as a malformed object slipping through. Wrap calls in try/catch when you need to handle failures gracefully:
try {
const response = await ContactsLayout.find({ query: { Status: "Active" } });
} catch (err) {
// A validation failure almost always means the layout changed
// since typegen last ran. Re-run typegen to catch up.
console.error("Failed to load contacts", err);
}
In practice, a validation error means the FileMaker layout no longer matches what Typegen generated — a field was renamed, removed, or its value list changed. Run Typegen again, then fix any TypeScript errors that surface in the app code.
Portal field names: removeFMTableNames
FileMaker's Data API returns portal row fields with the related table occurrence name prefixed onto the field name, e.g. "Orders::Status". The generated Z<Schema>Portals schemas already expect this shape for you, but if you're writing a Standard Schema by hand — or working with a raw response outside a generated client — @adt/fmdapi exports a helper to strip those prefixes:
removeFMTableNames({ "Orders::Status": "Shipped", count: 2 });
// => { Status: "Shipped", count: 2 }
It only strips the first TableName:: prefix, so a key like "Orders::Line::Notes" becomes "Line::Notes".
Inferring portal types: InferZodPortals
InferZodPortals<T> is the type helper that turns a Z<Schema>Portals object (a map of portal name to Zod schema) into its inferred output shape — this is exactly what generates T<Schema>Portals:
export type TContactsPortals = InferZodPortals<typeof ZContactsPortals>;
You'd reach for it directly only if you're building portal schemas by hand instead of relying on typegen's output.
Keeping schemas in sync
Typegen writes files into two places with different lifecycles:
-
generated/<Schema>.tsandclient/<Schema>.tsare rebuilt every time you run typegen. Never edit these — your changes are overwritten on the next run. -
<Schema>.tsat the root of your schema output is an override file, created once and left alone on later runs. It re-exports the generatedZ<Schema>/T<Schema>(and portal equivalents) under the same names, so this is the safe place to hand-adjust a schema — for example, tightening a Zod validator beyond what typegen infers from the layout.
Run Typegen again whenever a layout's fields, value lists, or portals change. See Generate types with Typegen for the workflow and adt.config.json for the configuration format.