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, date inputs, and validated number fields. Add one when the collection shape is stable.
Three sources, one resolution order
Section titled “Three sources, one resolution order”When Studio asks for a collection’s schema (GET /api/cms/schema?collection=pages), the server checks three sources in order. The first match wins.
| Order | Source | When used | Response source |
|---|---|---|---|
| 1 | Registered | Passed via the schemas option in caret() config |
"explicit" |
| 2 | Dynamic | Created via Studio’s collection builder (stored as metadata) | "dynamic" |
| 3 | Inferred | Guessed from the first stored entry | "inferred" |
Picking a source
Section titled “Picking a source”Registered Recommended
Section titled “Registered ”Pass JSON Schemas in your caret() config. You can write them directly or
convert Zod schemas with @caretcms/zod. If content.config.ts already
contains the collection’s Zod schema, reuse it here to keep one definition.
npm install @caretcms/zodimport { z } from 'zod';
export const PageSchema = z.object({ headline: z.string().min(1).meta({ title: 'Headline' }), intro: z.string().meta({ title: 'Intro paragraph' }), hero: z.object({ image: z.string().url().meta({ title: 'Hero image', format: 'url' }), alt: z.string().optional(), }), cta: z.enum(['primary', 'secondary', 'none']).meta({ title: 'CTA style' }),});
export const SiteSchema = z.object({ company: z.object({ name: z.string(), tagline: z.string(), }), social: z.object({ twitter: z.string().url().optional(), github: z.string().url().optional(), }),});Keep these in a plain module that imports only zod — not astro:content — so both content.config.ts and astro.config.mjs can import the same object.
import { defineConfig } from 'astro/config';import caret from '@caretcms/core';import { schemaFromZod } from '@caretcms/zod';import { PageSchema, SiteSchema } from './src/cms-schemas';
export default defineConfig({ output: 'server', integrations: [ caret({ schemas: { pages: schemaFromZod(PageSchema), site: schemaFromZod(SiteSchema), }, }), ],});Deriving a whole map at once? schemasFromZod({ pages: PageSchema, site: SiteSchema }) returns the full schemas object in one call.
Content-collection sites can derive the Studio schema from the same Zod schema used by the collection, avoiding a duplicate definition.
import { z } from 'zod';
// imported by BOTH content.config.ts and astro.config.mjsexport const blogSchema = z.object({ title: z.string().describe('Title'), description: z.string().describe('Description'), pubDate: z.date().describe('Publish date'), draft: z.boolean().default(false),});import { schemaFromZod } from '@caretcms/zod';import { blogSchema } from './src/schemas';
caret({ schemas: { blog: schemaFromZod(blogSchema) } });Pair this with the Markdown adapter and your src/content collection is editable in Studio with proper labels and types — single source of truth, no duplicated schema.
What this gets you:
- Typed inputs in Studio (string, number, boolean, enum, array, object)
- Required-field markers
- Validation on save with field-level error messages
- A canonical
templatepayload returned alongside the schema
Dynamic Editor-defined
Section titled “Dynamic ”Editors create new collections through /admin/cms/collections/new without you writing any code. The “schema builder” UI captures fields, types, and validation, then writes them into collection metadata via the create_collection mutation.
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 your editors need new content types and you don’t want to redeploy.
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 great for prototyping. Promote to a registered schema once the shape stabilizes.
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.
When to pick which
Section titled “When to pick which”| Situation | Use |
|---|---|
| You’re prototyping, schema unknown | Inferred (do nothing) |
| Schema is stable and code-owned | Registered |
| Editors need to define new content types | Dynamic |
| You want both code-owned core + editor-defined extras | Mix — register the core, let editors create dynamic ones |