Schema Types
Bunty includes a powerful schema validation system in @bunty/common. All schema types are built-in - no extra packages needed.
import { t } from '@bunty/common';
String
Basic String Validation
// Basic string
t.String()
// Required string
t.String().Required()
// Optional with default
t.String().Default('hello').Optional()
// With description
t.String().Description('User display name')
String Length
// Minimum length
t.String().MinLength(3)
// Maximum length
t.String().MaxLength(100)
// Exact length
t.String().Length(10)
// Between min and max
t.String().MinLength(5).MaxLength(50)
String Patterns
// Email validation
t.String().Email()
// URL validation
t.String().URL()
// UUID validation
t.String().UUID()
// ISO8601 date string
t.String().ISO8601()
// Custom regex pattern
t.String().Pattern(/^[A-Z][a-z]+$/)
// Alphanumeric only
t.String().Alphanumeric()
// Numeric string
t.String().Numeric()
String Transformations
// Trim whitespace
t.String().Trim()
// Convert to lowercase
t.String().Lowercase()
// Convert to uppercase
t.String().Uppercase()
// Remove HTML tags
t.String().StripHTML()
// Chain transformations
t.String().Trim().Lowercase().StripHTML()
Common String Patterns
// Phone number
t.String().Pattern(/^\+?[1-9]\d{1,14}$/)
// Username
t.String()
.MinLength(3)
.MaxLength(30)
.Pattern(/^[a-zA-Z0-9_-]+$/)
// Slug
t.String()
.MinLength(3)
.MaxLength(100)
.Pattern(/^[a-z0-9-]+$/)
// Hex color
t.String().Pattern(/^#[0-9a-fA-F]{6}$/)
// IP address
t.String().Pattern(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/)
Number
Basic Number Validation
// Basic number
t.Number()
// Required number
t.Number().Required()
// With default
t.Number().Default(0)
// Integer only
t.Number().Integer()
// Positive numbers
t.Number().Positive()
// Negative numbers
t.Number().Negative()
Number Range
// Minimum value
t.Number().Min(0)
// Maximum value
t.Number().Max(100)
// Between min and max
t.Number().Min(1).Max(10)
// Greater than (exclusive)
t.Number().GreaterThan(0)
// Less than (exclusive)
t.Number().LessThan(100)
Number Types
// Integer between 1 and 100
t.Number().Integer().Min(1).Max(100)
// Positive decimal
t.Number().Positive().Decimal()
// Age validation
t.Number().Integer().Min(0).Max(150)
// Percentage
t.Number().Min(0).Max(100)
// Price (2 decimal places)
t.Number().Positive().Decimal(2)
// Port number
t.Number().Integer().Min(1).Max(65535)
Boolean
// Basic boolean
t.Boolean()
// Required boolean
t.Boolean().Required()
// Default value
t.Boolean().Default(false)
// Truthy values: true, 'true', 1, 'yes'
// Falsy values: false, 'false', 0, 'no'
t.Boolean().Coerce()
Enum
String Enum
// Fixed set of values
t.Enum(['active', 'inactive', 'pending'])
// With default
t.Enum(['draft', 'published', 'archived'])
.Default('draft')
// Case-sensitive
t.Enum(['GET', 'POST', 'PUT', 'DELETE'])
TypeScript Enum
enum UserRole {
Admin = 'admin',
User = 'user',
Guest = 'guest'
}
t.Enum(UserRole)
.Default(UserRole.Guest)
enum HttpStatus {
OK = 200,
BadRequest = 400,
NotFound = 404
}
t.Enum(HttpStatus)
Array
Basic Array Validation
// Array of strings
t.Array(t.String())
// Array of numbers
t.Array(t.Number())
// Array of objects
t.Array(t.Object({
id: t.Number().Required(),
name: t.String().Required()
}))
// Nested arrays
t.Array(t.Array(t.String()))
Array Length
// Minimum length
t.Array(t.String()).MinLength(1)
// Maximum length
t.Array(t.String()).MaxLength(10)
// Exact length
t.Array(t.String()).Length(5)
// Between min and max
t.Array(t.String()).MinLength(1).MaxLength(100)
Array Operations
// Unique values only
t.Array(t.String()).Unique()
// Non-empty array
t.Array(t.String()).MinLength(1).Required()
// Filter out nulls/undefined
t.Array(t.String()).Compact()
// Transform items
t.Array(t.String())
.Transform(items => items.map(s => s.toLowerCase()))
Common Array Patterns
// Tags
t.Array(t.String().Trim().Lowercase())
.MinLength(1)
.MaxLength(10)
.Unique()
// IDs
t.Array(t.Number().Positive().Integer())
.MinLength(1)
.Unique()
// Email list
t.Array(t.String().Email())
.Unique()
// File paths
t.Array(t.String().Pattern(/^\/[\w\/.-]+$/))
Object
Basic Object Validation
// Simple object
t.Object({
name: t.String().Required(),
age: t.Number().Min(0).Optional()
})
// Nested objects
t.Object({
user: t.Object({
id: t.Number().Required(),
email: t.String().Email().Required()
}).Required(),
metadata: t.Object({
createdAt: t.String().ISO8601(),
updatedAt: t.String().ISO8601()
}).Optional()
})
Object Options
// All fields optional
t.Partial({
name: t.String(),
age: t.Number(),
email: t.String().Email()
})
// Pick specific fields
t.Pick(userSchema, ['name', 'email'])
// Omit specific fields
t.Omit(userSchema, ['password', 'salt'])
// Strict mode (no extra fields)
t.Object({
name: t.String()
}).Strict()
// Allow extra fields
t.Object({
name: t.String()
}).AllowExtra()
Common Object Patterns
// User object
t.Object({
id: t.Number().Positive().Integer().Required(),
email: t.String().Email().Lowercase().Required(),
username: t.String().MinLength(3).MaxLength(30).Required(),
role: t.Enum(['admin', 'user', 'guest']).Default('user'),
createdAt: t.String().ISO8601().Required()
})
// Address object
t.Object({
street: t.String().Required(),
city: t.String().Required(),
state: t.String().Length(2).Uppercase().Required(),
zipCode: t.String().Pattern(/^\d{5}(-\d{4})?$/).Required(),
country: t.String().Length(2).Uppercase().Default('US')
})
// Pagination
t.Object({
page: t.Number().Integer().Min(1).Default(1),
limit: t.Number().Integer().Min(1).Max(100).Default(20),
sortBy: t.String().Optional(),
order: t.Enum(['asc', 'desc']).Default('asc')
})
URL
// Basic URL
t.URL()
// Must be HTTPS
t.URL().Secure()
// Specific protocol
t.URL().Protocol('https')
// Specific domain
t.URL().Domain('example.com')
// With path pattern
t.URL().Path('/api/*')
// Common URL patterns
t.URL().Secure().Domain('api.example.com')
Date
// ISO8601 date string
t.Date()
// After specific date
t.Date().After('2024-01-01')
// Before specific date
t.Date().Before('2025-12-31')
// Between dates
t.Date()
.After('2024-01-01')
.Before('2025-12-31')
// Future dates only
t.Date().Future()
// Past dates only
t.Date().Past()
// Transform to Date object
t.Date().Transform(dateStr => new Date(dateStr))
JSON
// Any valid JSON
t.JSON()
// JSON with default
t.JSON().Default({ key: 'value' })
// JSON array
t.JSON().IsArray()
// JSON object
t.JSON().IsObject()
// Parse and validate
t.JSON().Schema(t.Object({
name: t.String().Required(),
age: t.Number().Min(0)
}))
Union
// String or number
t.Union([
t.String(),
t.Number()
])
// Multiple types
t.Union([
t.String().Email(),
t.String().URL(),
t.Null()
])
// Discriminated union
t.Union([
t.Object({
type: t.Literal('card'),
cardNumber: t.String().Required()
}),
t.Object({
type: t.Literal('paypal'),
email: t.String().Email().Required()
}),
t.Object({
type: t.Literal('bank'),
accountNumber: t.String().Required()
})
])
Literal
// Specific value
t.Literal('active')
// Numeric literal
t.Literal(42)
// Boolean literal
t.Literal(true)
// Use in discriminated unions
t.Object({
type: t.Literal('user'),
userId: t.Number().Required()
})
Nullable / Optional
// Nullable (can be null)
t.String().Nullable()
// Optional (can be undefined)
t.String().Optional()
// Both nullable and optional
t.String().Nullable().Optional()
// With default
t.String().Optional().Default('default value')
// Null or string
t.Union([t.String(), t.Null()])
Custom Validators
Synchronous
t.String().Custom((value) => {
if (value.includes('badword')) {
throw new Error('Content contains inappropriate language');
}
return value;
})
t.Number().Custom((value) => {
if (value % 2 !== 0) {
throw new Error('Must be an even number');
}
return value;
})
Asynchronous
t.String().Custom(async (username) => {
const exists = await checkUsernameExists(username);
if (exists) {
throw new Error('Username already taken');
}
return username;
})
t.String().Email().Custom(async (email) => {
const isDisposable = await checkDisposableEmail(email);
if (isDisposable) {
throw new Error('Disposable email addresses not allowed');
}
return email;
})
With Context
t.Number().Custom((total, context) => {
// Access other fields from context
const items = context.items;
const calculated = items.reduce((sum, item) => sum + item.price, 0);
if (Math.abs(total - calculated) > 0.01) {
throw new Error('Total does not match items');
}
return total;
})
Modifiers
Messages
t.String()
.MinLength(8)
.Message('Password must be at least 8 characters')
t.Number()
.Min(18)
.Message('You must be at least 18 years old')
t.String()
.Email()
.Message('Please enter a valid email address')
Transformations
// Transform after validation
t.String()
.Email()
.Transform(email => email.toLowerCase())
t.String()
.Pattern(/^\$?[\d,]+\.?\d*$/)
.Transform(price => parseFloat(price.replace(/[$,]/g, '')))
t.Array(t.String())
.Transform(tags => tags.map(tag => tag.trim().toLowerCase()))
.Unique()
Conditional Validation
// When field equals value
t.String()
.When('paymentMethod', 'card', t.String().Required())
// When field is truthy
t.String()
.WhenTruthy('requiresAddress', t.String().Required())
// When field is falsy
t.String()
.WhenFalsy('anonymous', t.String().Required())
// Custom condition
t.String()
.WhenCondition(
(data) => data.type === 'premium',
t.String().Required()
)
Type Inference
Bunty automatically infers TypeScript types from schemas:
const userSchema = t.Object({
id: t.Number().Positive().Integer(),
email: t.String().Email(),
username: t.String().MinLength(3),
age: t.Number().Min(18).Optional(),
role: t.Enum(['admin', 'user']).Default('user')
});
// Inferred type:
type User = Infer<typeof userSchema>;
// {
// id: number;
// email: string;
// username: string;
// age?: number;
// role: 'admin' | 'user';
// }
Best Practices
β Do
- Use descriptive field names
- Provide default values for optional fields
- Add clear error messages
- Validate ranges and patterns
- Transform data when needed
- Reuse common schemas
- Use type inference
β Donβt
- Skip validation for βtrustedβ input
- Use weak patterns (e.g.,
.+for email) - Ignore validation errors
- Mix validation with business logic
- Overcomplicate with excessive nesting
- Forget to sanitize user input
π― Built-in Power
All schema types are built into @bunty/common - no additional packages needed. Use them for validation, configuration, HTTP requests, and anywhere you need type-safe data validation.
Next Steps
- Apply schemas in Validation
- Use for Configuration
- Validate HTTP requests