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.
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 | "registered" |
| 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. The cleanest path is to write Zod schemas and convert them.
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(), }),});import { defineConfig } from 'astro/config';import caret from '@caretcms/core';import { z } from 'zod';import { PageSchema, SiteSchema } from './src/cms-schemas';
export default defineConfig({ output: 'server', integrations: [ caret({ schemas: { pages: z.toJSONSchema(PageSchema), site: z.toJSONSchema(SiteSchema), }, }), ],});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, "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. Anything outside this list is ignored at runtime (extra metadata is preserved through round-trips).
| Feature | Supported | Notes |
|---|---|---|
type: string | ✓ | with minLength, maxLength |
format: email / url | ✓ | maps to Zod .email() / .url() |
type: number / integer | ✓ | with minimum, maximum, int() |
type: boolean | ✓ | |
type: array | ✓ | with items |
type: object | ✓ | with nested properties, required |
enum | ✓ | string enums |
required | ✓ | array of property names |
title, description | ✓ | shown as labels and helper text |
default | ✓ | used in template generation |
oneOf, anyOf, allOf | · | not yet |
$ref | · | inline your schemas instead |
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 |