Skip to main content

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.

Building an app with an LLM?

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.

KeyApplies toEffect
enumstring, text, integer, decimal, booleanAllowed values → a TypeScript literal union (string, number, or boolean literals).
descriptionanySchema description → JSDoc on the generated type.
formatstring, textOpenAPI format hint (e.g. email, uri).
patternstring, textRegex the value is expected to match (advisory).
minimum, maximum, multipleOfinteger, decimalNumeric bounds.
minLength, maxLengthstring, textLength bounds.
properties, requiredobjectNested object shape → a typed interface (see above).
itemsarray-valued objectElement 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.

Output only

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.