Validation
Validation functions ensure data integrity through runtime type checking and cleanup operations. These utilities help you validate unknown data, remove unwanted values, and ensure objects meet expected schemas.
Table of Contents
validateShape
Runtime validation that checks if an object conforms to a schema, returning Result<T, ObjectError>.
Signature
function validateShape<T extends PlainObject>(
obj: unknown,
schema: ObjectSchema<T>
): Result<T, ObjectError>
type ObjectSchema<T extends PlainObject> = {
readonly [K in keyof T]: Validator<T[K]> | ObjectSchema<any>
}
type Validator<T> = (value: unknown) => value is T
interface ObjectError {
readonly code: 'PATH_NOT_FOUND' | 'INVALID_PATH' | 'VALIDATION_ERROR' | 'CIRCULAR_REFERENCE'
readonly message: string
readonly path?: ObjectPath
readonly cause?: unknown
}validateShape stops at the first failure, and the error it returns always has the code VALIDATION_ERROR.
Why validateShape?
// ❌ Without validation: runtime errors
function processConfig(config: unknown) {
const host = config.database.host // TypeError if config.database doesn't exist!
const port = config.database.port // TypeError!
return { host, port }
}
// ✅ With validateShape: safe, typed
const configSchema = {
database: {
host: (v: unknown): v is string => typeof v === 'string',
port: (v: unknown): v is number => typeof v === 'number'
}
}
const result = validateShape(config, configSchema)
if (isOk(result)) {
// Fields with a validator get its type. A nested schema's type is unknown,
// so pass a type argument to validateShape when you need it.
const { database } = result.value
console.log(database)
}Real-World: API Response Validation
import { map } from '@ontologyio/receta/result'
// GitHub API response validation
const githubUserSchema = {
login: (v: unknown): v is string => typeof v === 'string',
id: (v: unknown): v is number => typeof v === 'number',
avatar_url: (v: unknown): v is string => typeof v === 'string' && v.startsWith('http'),
public_repos: (v: unknown): v is number => typeof v === 'number' && v >= 0
}
const validateGitHubUser = (response: unknown) =>
pipe(
response,
(r) => validateShape(r, githubUserSchema),
map(user => ({
username: user.login,
id: user.id,
avatar: user.avatar_url,
repos: user.public_repos
}))
)Real-World: Environment Variable Validation
const envSchema = {
NODE_ENV: (v: unknown): v is string => ['development', 'production', 'test'].includes(v as string),
PORT: (v: unknown): v is string => typeof v === 'string' && /^\d+$/.test(v),
DATABASE_URL: (v: unknown): v is string => typeof v === 'string' && v.length > 0,
API_KEY: (v: unknown): v is string => typeof v === 'string' && v.length === 32
}
const loadEnv = () => {
const result = validateShape(process.env, envSchema)
return match(result, {
onOk: (env) => env,
onErr: (error) => {
console.error(`Invalid environment: ${error.message}`, error.path)
process.exit(1)
}
})
}Nested Schema Validation
// Validate nested structures
const userSchema = {
id: (v: unknown): v is number => typeof v === 'number',
profile: {
name: (v: unknown): v is string => typeof v === 'string' && v.length > 0,
email: (v: unknown): v is string => typeof v === 'string' && v.includes('@'),
age: (v: unknown): v is number => typeof v === 'number' && v >= 0 && v <= 150
},
settings: {
theme: (v: unknown): v is string => ['light', 'dark'].includes(v as string),
notifications: (v: unknown): v is boolean => typeof v === 'boolean'
}
}
const result = validateShape(unknownData, userSchema)
// Returns Err with path if nested validation fails:
// { code: 'VALIDATION_ERROR', path: ['profile', 'email'], message: 'Validation failed for key "email"' }Integration with Predicate Module
import { isString, isNumber } from '@ontologyio/receta/predicate'
const isEmail = (v: unknown): v is string => isString(v) && v.includes('@')
const userSchema = {
name: isString,
email: isEmail,
age: isNumber
}
validateShape(data, userSchema)Custom Validators
// Build reusable validators
const isPositiveInt = (v: unknown): v is number =>
typeof v === 'number' && Number.isInteger(v) && v > 0
const isNonEmptyString = (v: unknown): v is string =>
typeof v === 'string' && v.trim().length > 0
const isValidUrl = (v: unknown): v is string =>
typeof v === 'string' && /^https?:\/\/.+/.test(v)
const apiSchema = {
endpoint: isValidUrl,
timeout: isPositiveInt,
apiKey: isNonEmptyString
}Error Handling
const handleValidationError = (error: ObjectError) =>
`Validation failed at ${error.path?.join('.')}: ${error.message}`
const result = validateShape(data, userSchema)
if (isErr(result)) {
console.error(handleValidationError(result.error))
}stripUndefined
Removes all properties with undefined values from an object (shallow operation).
Signature
function stripUndefined<T extends PlainObject>(obj: T): Partial<T>Why stripUndefined?
// ❌ An undefined field is still a key: spreads and Object.keys see it
const payload = {
name: 'Alice',
email: undefined,
phone: undefined
}
Object.keys(payload)
// => ['name', 'email', 'phone']
// ✅ Clean payload before sending
const cleanPayload = stripUndefined(payload)
// => { name: 'Alice' }Real-World: Form Data Sanitization
import { flatMap } from '@ontologyio/receta/result'
// React form with optional fields
const FormComponent = () => {
const [formData, setFormData] = useState({
name: 'Alice',
email: 'alice@example.com',
phone: undefined, // Optional field not filled
company: undefined // Optional field not filled
})
const handleSubmit = async () => {
const cleanData = pipe(
formData,
stripUndefined,
(data) => validateShape(data, formSchema),
flatMap(saveToApi)
)
}
}Real-World: Query String Building
// Build URL query params, omit undefined
const buildQueryString = (params: Record<string, unknown>) => {
const clean = stripUndefined(params)
return new URLSearchParams(clean as Record<string, string>).toString()
}
const url = buildQueryString({
search: 'receta',
category: undefined, // Not set, should be omitted
page: 1,
sortBy: undefined // Not set, should be omitted
})
// => "search=receta&page=1"Real-World: Partial Updates
// Only send changed fields to API
const updateUser = (userId: string, changes: Partial<User>) => {
const payload = stripUndefined(changes)
// Only sends defined fields
return fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(payload)
})
}
// Usage
updateUser('123', {
name: 'Alice Smith', // Changed
email: undefined, // Not changed, omit
age: undefined // Not changed, omit
})Keeps Other Falsy Values
const data = {
count: 0, // Kept (falsy but not undefined)
active: false, // Kept (falsy but not undefined)
name: '', // Kept (falsy but not undefined)
email: null, // Kept (not undefined)
phone: undefined // Removed
}
stripUndefined(data)
// => { count: 0, active: false, name: '', email: null }In Pipelines
// Complete sanitization pipeline
const sanitizeFormData = (formData: Record<string, unknown>) =>
pipe(
formData,
stripUndefined, // Remove undefined
filterValues((v) => v !== ''), // Remove empty strings
mask(allowedFields), // Only allowed fields
validateShape(formSchema) // Validate structure
)compact
Removes all nullish (null or undefined) values from an object (shallow operation).
Signature
function compact<T extends PlainObject>(obj: T): Partial<T>Why compact?
// ❌ Nullish values can cause issues downstream
const userData = {
name: 'Alice',
email: null, // User cleared email
phone: undefined, // Never provided
age: 30
}
// ✅ Remove all nullish values
const clean = compact(userData)
// => { name: 'Alice', age: 30 }stripUndefined vs compact
const data = {
a: undefined,
b: null,
c: 0,
d: false,
e: ''
}
// Keeps null
stripUndefined(data)
// => { b: null, c: 0, d: false, e: '' }
// Removes both null and undefined
compact(data)
// => { c: 0, d: false, e: '' }Real-World: Database Query Building
// Build WHERE clause, skip nullish filters
const buildWhereClause = (filters: {
status?: string | null
category?: string | null
minPrice?: number | null
}) => {
const activeFilters = compact(filters)
return Object.entries(activeFilters)
.map(([key, value]) => `${key} = '${value}'`)
.join(' AND ')
}
buildWhereClause({
status: 'active',
category: null, // Skip this
minPrice: undefined // Skip this
})
// => "status = 'active'"Real-World: GraphQL Query Variables
// Remove nullish variables before sending GraphQL query
const queryUsers = (filters: UserFilters) => {
const variables = compact(filters)
return graphql`
query Users($status: String, $role: String, $search: String) {
users(status: $status, role: $role, search: $search) {
id
name
}
}
`({
variables // Only includes non-nullish values
})
}Real-World: localStorage Cleanup
// Save only meaningful preferences
const savePreferences = (prefs: UserPreferences) => {
const meaningful = pipe(
prefs,
compact, // Remove nullish
filterValues((v) => v !== '') // Remove empty strings
)
localStorage.setItem('preferences', JSON.stringify(meaningful))
}Preserves Zero, False, Empty String
// compact only removes nullish, not other falsy values
const data = {
count: 0, // ✅ Kept (valid number)
enabled: false, // ✅ Kept (valid boolean)
title: '', // ✅ Kept (valid string)
notes: null, // ❌ Removed (nullish)
tags: undefined // ❌ Removed (nullish)
}
compact(data)
// => { count: 0, enabled: false, title: '' }In Pipelines
// Complete data cleanup
const prepareAnalytics = (rawData: RawAnalytics) =>
pipe(
rawData,
compact, // Remove nullish
(data) => flatten(data), // Flatten for storage
filterKeys((k) => !k.includes('_internal')) // Remove internal keys
)When to Use Which
Decision Tree
Need to validate or clean object?
│
├─ Need runtime type checking?
│ → Use validateShape() → Result<T, ObjectError>
│
├─ Have optional form fields?
│ │
│ ├─ Only remove undefined?
│ │ → Use stripUndefined()
│ │
│ └─ Remove both null and undefined?
│ → Use compact()
│
└─ Building API payload?
└─ Use stripUndefined() + validateShape()Comparison Table
| Function | Removes | Returns | Use Case |
|---|---|---|---|
validateShape | - | Result<T, E> | Type checking unknown data |
stripUndefined | undefined | Partial<T> | Form data, API payloads |
compact | null + undefined | Partial<T> | Query builders, cleanup |
Common Patterns
import { flatMap, map, unwrapOr } from '@ontologyio/receta/result'
// Pattern 1: Form submission
pipe(
formData,
stripUndefined,
validateShape(formSchema),
flatMap(submitToApi)
)
// Pattern 2: Query building
pipe(
filters,
compact,
buildWhereClause
)
// Pattern 3: API response cleanup
pipe(
apiResponse,
compact,
validateShape(responseSchema),
map(normalizeData)
)
// Pattern 4: Configuration loading
pipe(
rawConfig,
stripUndefined,
validateShape(configSchema),
unwrapOr(defaultConfig)
)Integration with Validation Module
The Object module's validateShape is designed for structural validation (type checking), while the Validation module handles business logic validation (error accumulation).
When to Use Object.validateShape
// ✅ Type checking unknown API responses
const result = validateShape(apiResponse, userSchema)
// => Result<User, ObjectError> (fail-fast)
// ✅ Runtime type guards
const checked = validateShape(data, userSchema)
if (isOk(checked)) {
// checked.value has the schema's shape
}When to Use Validation Module
import { schema, validate, required, email, minLength, min } from '@ontologyio/receta/validation'
// ✅ Business rules with error accumulation (fail-slow)
const userFormSchema = {
name: validate([required('Name is required'), minLength(3, 'Name too short')]),
email: validate([required('Email is required'), email('Invalid email')]),
age: min(18, 'Must be 18+')
}
// All errors at once
const result = schema(userFormSchema, { name: 'Al', email: 'nope', age: 17 })
// => Invalid([{ field: 'name', errors: ['Name too short'] }, { field: 'email', errors: ['Invalid email'] }, { field: 'age', errors: ['Must be 18+'] }])Using Both Together
import { flatMap, fromResult } from '@ontologyio/receta/validation'
// Step 1: Structural validation (fail-fast)
const structuralCheck = validateShape(unknownData, typeSchema)
// Step 2: Business validation (fail-slow)
const businessCheck = pipe(
fromResult(structuralCheck),
flatMap(validateBusinessRules)
)Next Steps
- Transformation - mapKeys, mapValues, filterKeys, filterValues
- Patterns & Recipes - Complete real-world solutions
- API Reference - Quick lookup and cheat sheets
Safe Access
Safe access functions provide type-safe ways to read and modify deeply nested object structures without runtime errors. All operations are immutable and integrate with the Option pattern.
Transformation
Transformation functions allow you to systematically transform or filter object keys and values. These operations are essential for normalizing data, applying bulk transformations, and filtering unwanted properties.