Skip to content

Schemas

Schemas tell Studio what fields an entry has, what type each one is, and how to label them. They’re optional — CaretCMS works with no schema at all by inferring shape from your stored data.

But schemas turn Studio from a JSON editor into a typed form: dropdowns for enums, date pickers for dates, validated number ranges. Worth setting up once you know your shape.

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.

OrderSourceWhen usedResponse source
1RegisteredPassed via the schemas option in caret() config"registered"
2DynamicCreated via Studio’s collection builder (stored as metadata)"dynamic"
3InferredGuessed from the first stored entry"inferred"

Registered Recommended

Section titled “Registered ”

Pass JSON Schemas in your caret() config. The cleanest path is to write Zod schemas and convert them.

src/cms-schemas.ts
import { 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(),
}),
});

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 template payload 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,
"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.

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 valueInferred type
"hello"string
42number
trueboolean
["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.

Every schema response includes a template — a default value for new entries. Studio uses it when an editor clicks “New entry”:

Field typeTemplate value
string""
number0
booleanfalse
object{} with each property recursively templated
array[]
enumFirst value

You can override per-field via default in JSON Schema (or .default(value) in Zod).

CaretCMS understands a focused subset of JSON Schema. Anything outside this list is ignored at runtime (extra metadata is preserved through round-trips).

FeatureSupportedNotes
type: stringwith minLength, maxLength
format: email / urlmaps to Zod .email() / .url()
type: number / integerwith minimum, maximum, int()
type: boolean
type: arraywith items
type: objectwith nested properties, required
enumstring enums
requiredarray of property names
title, descriptionshown as labels and helper text
defaultused in template generation
oneOf, anyOf, allOf·not yet
$ref·inline your schemas instead
SituationUse
You’re prototyping, schema unknownInferred (do nothing)
Schema is stable and code-ownedRegistered
Editors need to define new content typesDynamic
You want both code-owned core + editor-defined extrasMix — register the core, let editors create dynamic ones