Validation
Attribute validation is written with Zod in flexkit.config.ts, compiled to JSON Schema by flexkit deploy, and enforced by the Studio form. You author the rule; the Studio generates the message.
Authoring
validation receives the Zod namespace and returns a schema:
import { defineEntity } from '@flexkit/schema';
export const products = defineEntity({
name: 'product',
plural: 'products',
attributes: [
{
name: 'name',
label: 'Name',
scope: 'local',
dataType: 'string',
inputType: 'text',
validation: (z) => z.string().min(1),
},
{
name: 'code',
label: 'Code',
scope: 'global',
dataType: 'string',
inputType: 'text',
validation: (z) => z.string().min(3).max(12).regex(/^[A-Z0-9-]+$/),
},
{
name: 'contactEmail',
label: 'Email',
scope: 'global',
dataType: 'string',
inputType: 'text',
validation: (z) => z.email(),
},
{
name: 'rating',
label: 'Rating',
scope: 'global',
dataType: 'int',
inputType: 'number',
validation: (z) => z.number().int().min(1).max(5),
},
{
name: 'status',
label: 'Status',
scope: 'global',
dataType: 'string',
inputType: 'select',
options: {
list: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
},
validation: (z) => z.enum(['draft', 'published']),
},
],
});Do not pass messages: z.string().min(1, { error: 'Name is required' }) deploys, but the string is ignored. See Generated messages.
What deploy does
flexkit deploy calls each validation function, converts the resulting Zod schema to a JSON Schema (draft-07) fragment and sends that fragment with the attribute. The config file itself never reaches the server.
Before anything is sent, the CLI checks that every fragment only uses rules the Studio form can enforce. A rule it cannot express fails the deploy locally with an error that names the entity and attribute, for example:
Attribute "price" of entity "product": validation uses refine(), superRefine() or check(), which cannot be expressed as JSON Schema. Use built-in Zod checks such as min(), max(), length(), regex() or email().The backend compiles every fragment again with Ajv and rejects a broken one with a 422 at schema[i].attributes[j].validation, so a hand-edited or outdated payload cannot deploy a rule the form would then ignore.
Supported rules
The complete keyword allowlist is type, minLength, maxLength, pattern, format, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, enum, const, properties, required, additionalProperties, items, minItems, maxItems, anyOf, oneOf, not, description and title. Supported formats are email, uri, uuid, date-time, date and time.
Unsupported rules
These fail the deploy before anything is sent:
Rules the form cannot enforce belong in an automation that checks writes, or in a skill that teaches agents the constraint.
Generated messages
The Studio does not use authored messages. It builds one message per field from the attribute label and the failing rule, and shows the first failure:
An empty value on a field whose rule rejects it always shows “<Label> is required”. Choose labels that read well in a sentence.
Zod 4 ordering. .int() returns a fresh schema, so checks chained before it are lost. Write
z.number().int().min(0).max(5), not z.number().min(0).max(5).int().
Root-level .optional() has no JSON Schema equivalent and is dropped. That is rarely a problem: an empty string still
passes a plain z.string(), and only .min(1) makes a text field required. Inside z.object() properties,
.optional() is kept as usual.
Where validation runs today
- Studio form: enforced. The form converts the deployed fragment back to a validator; hidden and read-only fields, and fields on hidden field groups, are skipped.
- GraphQL API, imports, automations: not enforced yet. API clients can write values the form would reject. Use
isRequired: truewhen a value must exist at the API level (the generated field becomes non-null), and keep critical invariants in an automation until API-level enforcement ships.
How agents see it
The deployed fragment is part of the project schema. Agents and API clients read it through the schema endpoint (GET /<projectId>/api/schema) and through the MCP search_schema tool, so a well-labelled rule doubles as documentation: an agent asked to create a product learns that rating is an integer between 1 and 5 before it writes.