Skip to content

Validation Helpers

ctkit ships two sets of helper functions that return validation objects. Use them instead of writing raw validation objects to keep your schemas readable and typo-free.

import { validators, richTextValidators } from "@ctkit/core";

All text validators return a TextValidation object.

Validates that the value matches snake_case format.

validators.snakeCase()
// → { regexp: { pattern: "^[a-z][a-z0-9_]*$" } }
{
id: "apiKey",
name: "API Key",
type: "Symbol",
required: true,
validations: [validators.snakeCase()],
}

Validates that the value matches kebab-case format.

validators.kebabCase()
// → { regexp: { pattern: "^[a-z][a-z0-9-]*$" } }

Validates that the value matches camelCase format.

validators.camelCase()
// → { regexp: { pattern: "^[a-z][a-zA-Z0-9]*$" } }

Validates a URL-safe slug (lowercase-words-separated-by-hyphens).

validators.slug()
// → { regexp: { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" } }
{
id: "slug",
name: "Slug",
type: "Symbol",
required: true,
validations: [validators.slug(), validators.unique()],
}

Validates a basic email address format.

validators.email()
// → { regexp: { pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" } }

Validates that the value starts with http:// or https://.

validators.url()
// → { regexp: { pattern: "^https?:\\/\\/.+" } }

Validates a 6-digit hex color code (e.g. #FF5733).

validators.hexColor()
// → { regexp: { pattern: "^#[0-9A-Fa-f]{6}$" } }
{
id: "brandColor",
name: "Brand Color",
type: "Symbol",
required: false,
validations: [validators.hexColor()],
}

Validates a standard UUID format (case-insensitive).

validators.uuid()
// → { regexp: { pattern: "^[0-9a-f]{8}-...", flags: "i" } }

Restricts string length. Both parameters are optional.

validators.textLength(1, 120)
// → { size: { min: 1, max: 120 } }
validators.textLength(10)
// → { size: { min: 10 } }
validators.textLength(undefined, 500)
// → { size: { max: 500 } }

Restricts the value to an array of allowed strings.

validators.textIn(["draft", "review", "published"])
// → { in: ["draft", "review", "published"] }
{
id: "status",
name: "Status",
type: "Symbol",
required: true,
validations: [validators.textIn(["active", "inactive", "archived"])],
}

Marks the field as unique across all entries.

validators.unique()
// → { unique: true }

Also available as the more specific uniqueText():

validators.uniqueText()
// → { unique: true }

Validates against a custom regular expression.

validators.customRegex("^[A-Z]{2}-\\d{4}$")
// → { regexp: { pattern: "^[A-Z]{2}-\\d{4}$" } }
validators.customRegex("^(foo|bar)$", "i")
// → { regexp: { pattern: "^(foo|bar)$", flags: "i" } }

All number validators return a NumberValidation object.

Restricts the number to a range. Both parameters are optional.

validators.numberRange(1, 100)
// → { range: { min: 1, max: 100 } }
validators.numberRange(0)
// → { range: { min: 0 } }
{
id: "rating",
name: "Rating",
type: "Integer",
required: true,
validations: [validators.numberRange(1, 5)],
}

Restricts the value to an array of allowed numbers.

validators.numberIn([1, 2, 3, 4, 6, 12])
// → { in: [1, 2, 3, 4, 6, 12] }

Marks the number field as unique across all entries.

validators.uniqueNumber()
// → { unique: true }

Restricts the number of items in an array field.

validators.arraySize(1, 10)
// → { size: { min: 1, max: 10 } }
{
id: "tags",
name: "Tags",
type: "Array",
required: false,
items: { type: "Symbol" },
validations: [validators.arraySize(1, 5)],
}

All rich text validators return a RichTextValidation object.

import { richTextValidators } from "@ctkit/core";

Enables only bold, italic, underline, and strikethrough marks.

richTextValidators.basicFormatting()
// → { enabledMarks: ["bold", "italic", "underline", "strikethrough"] }
{
id: "summary",
name: "Summary",
type: "RichText",
required: true,
validations: [richTextValidators.basicFormatting()],
}

Restricts the field to plain paragraphs with no formatting at all.

richTextValidators.paragraphsOnly()
// → { enabledNodeTypes: ["paragraph", "text"], enabledMarks: [] }

Allows all standard block elements except headings.

richTextValidators.noHeadings()
// → {
// enabledNodeTypes: [
// "paragraph", "ordered-list", "unordered-list",
// "list-item", "blockquote", "hr", "text"
// ]
// }

Allows paragraphs plus only the specified heading levels.

richTextValidators.headingLevels([2, 3])
// → { enabledNodeTypes: ["paragraph", "text", "heading-2", "heading-3"] }
{
id: "body",
name: "Body",
type: "RichText",
required: true,
validations: [
richTextValidators.headingLevels([2, 3, 4]),
richTextValidators.basicFormatting(),
],
}

Allows all standard text and structure nodes but disables all embedded entries, embedded assets, and embedded resources.

richTextValidators.noEmbeddedContent()
// → {
// enabledNodeTypes: [
// "paragraph", "heading-1", "heading-2", "heading-3",
// "heading-4", "heading-5", "heading-6",
// "ordered-list", "unordered-list", "list-item",
// "blockquote", "hr", "text"
// ]
// }

Restricts which content types can be embedded as block or inline entries.

richTextValidators.embeddedEntries(["cta", "codeBlock"])
// → {
// nodes: {
// "embedded-entry-block": [{ linkContentType: ["cta", "codeBlock"] }],
// "embedded-entry-inline": [{ linkContentType: ["cta", "codeBlock"] }],
// }
// }
{
id: "body",
name: "Body",
type: "RichText",
required: true,
validations: [
richTextValidators.embeddedEntries(["callout", "videoEmbed"]),
],
}

Enables exactly the specified inline marks.

richTextValidators.allowedMarks(["bold", "italic", "code"])
// → { enabledMarks: ["bold", "italic", "code"] }

Enables exactly the specified node types.

richTextValidators.allowedNodeTypes([
"paragraph",
"heading-2",
"heading-3",
"unordered-list",
"hyperlink",
"text",
])
// → { enabledNodeTypes: ["paragraph", "heading-2", ...] }

Validators compose naturally — just spread them into the validations array:

import { validators, richTextValidators } from "@ctkit/core";
import type { ContentTypeSchema } from "@ctkit/core";
const article: ContentTypeSchema = {
id: "article",
name: "Article",
displayField: "title",
fields: [
{
id: "title",
name: "Title",
type: "Symbol",
required: true,
validations: [
validators.textLength(1, 120),
validators.unique(),
],
},
{
id: "slug",
name: "Slug",
type: "Symbol",
required: true,
validations: [validators.slug(), validators.unique()],
},
{
id: "body",
name: "Body",
type: "RichText",
required: true,
validations: [
richTextValidators.headingLevels([2, 3]),
richTextValidators.basicFormatting(),
richTextValidators.embeddedEntries(["cta", "codeBlock"]),
],
},
{
id: "rating",
name: "Rating",
type: "Integer",
required: false,
validations: [validators.numberRange(1, 5)],
},
{
id: "tags",
name: "Tags",
type: "Array",
required: false,
items: { type: "Symbol" },
validations: [validators.arraySize(1, 10)],
},
],
};
export default article;