πŸ“¦
Bunty

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

Have questions? Join our Discord community
Found an issue? Edit this page on GitHub