Field Spec
A field spec attaches optional OpenAPI/JSON-Schema hints to a property — an allowed-values set (enum), a description, numeric or length bounds, or (for object fields) a full nested shape. They are merged into the generated OpenAPI document (/api/_openapi) and the generated TypeScript types (types.d.ts), so typed clients — and code-generating LLMs — get a stricter contract.
A field spec is output only — it enriches the published contract, it is not validated on input. The API still accepts any value the underlying type allows. The point is to give typed clients — and code-generating LLMs — a stricter contract to work against.
Why
A plain string field tells a client nothing about which values are valid. Add an enum and the generated TypeScript type becomes a string-literal union:
// Without a field spec
status: string;
// With spec.enum = ["pending", "paid", "shipped"]
status: "pending" | "paid" | "shipped";
Now a client that writes status: "complete" fails at compile time — even though the API would accept it. A description becomes a JSDoc comment on the generated type, so it surfaces in editor tooltips and tells an LLM how to use the field.
Typing object fields
An object field is schema-less by default — clients see Record<string, unknown>. Give it a spec with properties (standard JSON-Schema) and it becomes a real typed object:
{
"name": "address",
"type": "object",
"spec": {
"properties": {
"street": { "type": "string" },
"zip": { "type": "string" },
"country": { "type": "string", "enum": ["NO", "SE", "DK"] }
},
"required": ["street", "zip"]
}
}
// Without a spec
address?: Record<string, unknown>;
// With the spec above
address?: { street: string; zip: string; country?: "NO" | "SE" | "DK" };
Nested objects and arrays work too — the spec is just a partial JSON-Schema fragment, so properties, items, and per-field enum/format/description nest as deep as you need.
This is the highest-value use of field specs. When you define a collection with object fields, give each one a spec.properties — the generated types.d.ts hands your app fully typed objects instead of Record<string, unknown>, so the compiler catches shape mistakes for free.
Setting a spec
Add a spec object to the property. Every key is optional.
{
"name": "status",
"type": "string",
"spec": {
"enum": ["pending", "paid", "shipped"],
"description": "Order lifecycle status"
}
}
In the portal, open a property and find the Field spec (advanced) section.
Keys
A spec is a partial JSON-Schema fragment, so any standard OpenAPI keyword below is honored.
| Key | Applies to | Effect |
|---|---|---|
enum | string, text, integer, decimal, boolean | Allowed values → a TypeScript literal union (string, number, or boolean literals). |
description | any | Schema description → JSDoc on the generated type. |
format | string, text | OpenAPI format hint (e.g. email, uri). |
pattern | string, text | Regex the value is expected to match (advisory). |
minimum, maximum, multipleOf | integer, decimal | Numeric bounds. |
minLength, maxLength | string, text | Length bounds. |
properties, required | object | Nested object shape → a typed interface (see above). |
items | array-valued object | Element schema for an array. |
A spec can be set on string, text, integer, decimal, boolean, date, date-time and object properties. It is not available on guid, blob or lookup properties.
Regenerate your client
The spec flows into both /api/_openapi and the generated types.d.ts. After changing a spec, regenerate your client (e.g. gg rat api -a <api> types, or your OpenAPI generator) to pick up the new types.
A field spec does not reject input. If a value must be one of the allowed values at write time, enforce it in a function or validate it client-side — the spec is a typing aid, not a constraint.