Schemas
Schemas tell Studio which fields an entry has, their types, and their labels. They are optional: without one, CaretCMS infers a schema from stored data.
A schema lets Studio render typed controls such as enum menus, image controls, and validated number fields. Add one when the collection shape is stable.
Registered
Section titled “Registered”Start with a registered schema: a field definition you keep in your Astro
configuration. This complete example creates a clear Studio form for a pages
collection without adding another package:
import { defineConfig } from 'astro/config';import caret, { filesystemStorage } from '@caretcms/core';
export default defineConfig({ integrations: [caret({ delivery: 'static', storage: filesystemStorage(), schemas: { pages: { type: 'object', properties: { headline: { type: 'string', title: 'Headline', minLength: 1 }, intro: { type: 'string', title: 'Introduction', format: 'textarea' }, image: { type: 'string', title: 'Hero image', format: 'image' }, alt: { type: 'string', title: 'Alternative text' }, }, required: ['headline'], }, }, })],});Keep your existing delivery, storage, and other integrations if you already have
a working configuration; the relevant addition is schemas.pages. Bind a field
with data-caret="pages::home::headline", where home is the entry ID.
type selects the kind of value, title supplies its label, and required
lists the fields that must exist. minLength: 1 also prevents an empty headline.
A schema defines an entry’s shape; it does not create page routes or seed content.
Use an existing Zod schema
Section titled “Use an existing Zod schema”Zod is optional. If your project already uses Zod 4, the companion package converts those definitions into the JSON Schema CaretCMS accepts:
npm install @caretcms/zod@0.5.0 zod@4First, keep the shared definition in a plain module:
import { z } from 'zod';
export const PageSchema = z.object({ headline: z.string().min(1).meta({ title: 'Headline' }), intro: z.string().meta({ title: 'Introduction', format: 'textarea' }), image: z.string().meta({ title: 'Hero image', format: 'image' }), alt: z.string().meta({ title: 'Alternative text' }),});Then import and convert it in your existing configuration:
import { schemaFromZod } from '@caretcms/zod';import { PageSchema } from './src/cms-schemas';
// Inside your existing caret({...}) call:// schemas: { pages: schemaFromZod(PageSchema) }For several collections, schemasFromZod({ pages: PageSchema, site: SiteSchema })
converts a map of your definitions. Keep shared schemas independent of
astro:content, so both src/content.config.ts and astro.config.mjs can import
them. A schema in Astro’s content config is not automatically registered with
CaretCMS; pass it through the schemas option too.
The helper promotes .describe() to a label when no explicit title is set.
It converts z.date() into a string schema with format: 'date'; Studio still
uses a text input, not a date picker. When validating date strings with Zod,
choose a string schema or an appropriate coercion for your data source.
CaretCMS enforces its supported JSON Schema subset, not arbitrary Zod refinements
or transformation functions.
Three sources, one resolution order
Section titled “Three sources, one resolution order”Studio uses the first available source for each collection:
| Source, in priority order | Where it comes from |
|---|---|
| Registered | The schemas option in caret() |
| Dynamic | Collection metadata created through the authenticated API |
| Inferred | Field types guessed from the first stored entry |
The schema endpoint reports these as explicit, dynamic, and inferred.
Registering a schema overrides a dynamic one without deleting its entries.
For most sites, start with registered schemas and use inference while prototyping.
Picking a source
Section titled “Picking a source”Use registered schemas for fields you maintain in code. Use dynamic schemas when a custom application must define collections at runtime. Leave schemas unset for a quick prototype, then register them once the field structure is settled.
Dynamic
Section titled “Dynamic”Custom clients create collections with the create_collection mutation.
Studio has no collection-builder screen in 0.5.0. After the API creates metadata,
Studio can list the collection and edit its entries.
See Dynamic Collections for the full flow. The stored metadata looks like:
{ "id": "products", "label": "Products", "icon": "📦", "creatable": true, "orderable": true, "deletable": true, "order": 20, "schema": { "type": "object", "properties": { "name": { "type": "string", "title": "Product name" }, "price": { "type": "number", "minimum": 0, "title": "Price (USD)" }, "in_stock": { "type": "boolean", "title": "In stock" } }, "required": ["name", "price"] }, "created_at": 1714225200000, "updated_at": 1714225200000}Use this when an authenticated custom client needs to define new content types without redeploying the integration. Public templates still need to render them.
Inferred
Section titled “Inferred”If no schema is registered and no metadata exists, Studio reads the first entry in the collection and guesses field types from JS values:
| JS value | Inferred type |
|---|---|
"hello" |
string |
42 |
number |
true |
boolean |
["a", "b"] |
array of string |
{ x: 1 } |
object with inferred properties |
Inference is useful for trying an existing collection. An empty collection has no fields to infer; register a schema before asking editors to create its entries.
Make forms understandable
Section titled “Make forms understandable”Use title for a short field label and description for a specific explanation.
Add x-caret-groups to an object schema to group its immediate properties:
const pageSchema = { type: 'object', 'x-caret-groups': [ { title: 'Hero', description: 'The first content visitors see.', fields: ['headline', 'cover', 'alt'] }, { title: 'Body', fields: ['body'] }, ], properties: { headline: { type: 'string', title: 'Headline', minLength: 1 }, cover: { type: 'string', title: 'Cover image', format: 'image' }, alt: { type: 'string', title: 'Alternative text', description: 'Describe the image for someone who cannot see it.' }, body: { type: 'string', title: 'Body', format: 'html' }, }, required: ['headline'],};
caret({ schemas: { pages: pageSchema } });Ungrouped properties remain available in an additional section. Groups organize controls; they do not change the stored object shape or validation rules.
| Format | Studio control |
|---|---|
image |
Image preview, replacement upload, secondary URL entry |
html |
Sanitized formatted text with optional Edit HTML source mode |
textarea |
Multiline text; multiline stored strings also use this control |
date / date-time |
String input; no dedicated date picker |
email / url / uri |
Suitable text input |
Alternative text should be a visible schema field used by your template’s alt
attribute. Do not rely on image replacement to invent meaningful alt text.
What template does
Section titled “What template does”Every schema response includes a template — a default value for new entries. Studio uses it when an editor clicks “New entry”:
| Field type | Template value |
|---|---|
string |
"" |
number |
0 |
boolean |
false |
object |
{} with each property recursively templated |
array |
[] |
enum |
First value |
You can override per-field via default in JSON Schema (or .default(value) in Zod).
JSON Schema features supported
Section titled “JSON Schema features supported”CaretCMS understands a focused subset of JSON Schema for Studio controls, templates, and dependency-free server validation. Extra metadata is preserved, but unsupported validation keywords are not enforced.
| Feature | Supported | Notes |
|---|---|---|
type: string |
Yes | validates minLength, maxLength, and pattern |
format: email / url |
Studio control | chooses the input type; format itself is not server-validated |
type: number / integer |
Yes | validates minimum, maximum, exclusive bounds, and integer values |
type: boolean |
Yes | |
type: array |
Yes | validates items, minItems, and maxItems recursively |
type: object |
Yes | validates nested properties, required, and additionalProperties: false |
enum, const |
Yes | exact-value validation |
required |
Yes | array of property names |
title, description |
Yes | shown as labels and helper text |
default |
Yes | used in template generation |
oneOf, anyOf |
Yes | value must match at least one alternative |
allOf |
Yes | every child schema is applied |
local $ref |
Yes | #/... references only; remote references are not fetched |
Validation failures return structured issues with path, code, and message
so Studio can place an error beside the exact nested field. The server validates
put_entry, field saves, and every affected entry in a reorder before any write
is committed.