Skip to content

Schema & Validation Utilities

Schemas and their validation are a common requirement in API development. APIful provides a comprehensive set of utilities for creating and validating schemas using JSON Schema and TypeScript, providing both type safety and runtime validation.

Core Concepts

Validators

The foundation of the validation system is the Validator<T> interface, which provides type-safe validation capabilities:

ts
interface Validator<T = unknown> {
  /**
   * Optional. Validates that the structure of a value matches this schema,
   * and returns a typed version of the value if it does.
   */
  readonly validate?: (value: unknown) => ValidationResult<T>
}

Schemas

Schemas extend validators with JSON Schema support:

ts
interface Schema<T = unknown> extends Validator<T> {
  /**
   * Schema type for inference.
   */
  _type: T

  readonly jsonSchema: JSONSchema7
}

Creating Schemas

Create schemas using the jsonSchema utility function:

ts
import { jsonSchema } from 'apiful/utils'

const userSchema = jsonSchema<{
  id: number
  name: string
}>({
  type: 'object',
  properties: {
    id: { type: 'number' },
    name: { type: 'string' }
  },
  required: ['id', 'name']
})

TIP

Always provide the TypeScript type as a generic parameter to ensure type safety between your JSON Schema and TypeScript definitions.

Custom Validation Logic

You can provide custom validation logic alongside the JSON Schema:

ts
const userSchema = jsonSchema<User>(
  {
    type: 'object',
    properties: {
      id: { type: 'number' },
      name: { type: 'string' }
    },
    required: ['id', 'name']
  },
  {
    validate: (value) => {
      if (myValidationLogic(value)) {
        return { success: false, error: new Error('Custom validation failed') }
      }
      return { success: true, value: value as User }
    }
  }
)

Validation

Unsafe Validation

Use the validateTypes function for scenarios where validation failures should throw errors:

ts
import { validateTypes } from 'apiful/utils'

try {
  const user = validateTypes({
    value: data,
    schema: userSchema
  })
  // user is typed as { id: number, name: string }
}
catch (error) {
  if (error instanceof TypeValidationError) {
    console.error('Validation failed:', error.value, error.cause)
  }
}

Safe Validation

For graceful error handling, use safeValidateTypes to handle validation results without throwing:

ts
import { safeValidateTypes } from 'apiful/utils'

const result = safeValidateTypes({
  value: data,
  schema: userSchema
})

if (result.success) {
  // `result.value` is typed data
  console.log(result.value.name)
}
else {
  // `TypeValidationError` with details
  console.error(result.error.value, result.error.cause)
}

NOTE

Safe validation is particularly useful in API endpoints where you want to return validation errors to the client instead of crashing the server.

Custom Validators

Create standalone validators without JSON Schema using the validator function:

ts
import { validator } from 'apiful/utils'

const emailValidator = validator<string>((value) => {
  if (typeof value !== 'string' || !value.includes('@'))
    return { success: false, error: new Error('Invalid email') }

  return { success: true, value }
})

TIP

Custom validators are perfect for business logic validation that goes beyond JSON Schema capabilities, such as checking database constraints or complex business rules.

Type Guards

Schema Type Guard

Ensure a value is a valid Schema using the isSchema type guard:

ts
import { isSchema } from 'apiful/utils'

if (isSchema(value)) {
  console.log(value.jsonSchema)
}

Validator Type Guard

Ensure a value is a valid Validator using the isValidator type guard:

ts
import { isValidator } from 'apiful/utils'

if (isValidator(value)) {
  if (value.validate) {
    const result = value.validate(someValue)
  }
}

Error Handling

TypeValidationError

Thrown during validation failures with detailed context:

ts
declare class TypeValidationError extends Error {
  readonly value: unknown // The value that failed validation
  readonly cause?: unknown // The underlying cause of the failure
}

Error Messages

Error messages are automatically formatted to include:

  • The invalid value (JSON stringified)
  • The cause of the validation failure
  • Proper error message extraction from various error types

Best Practices

  1. Type Safety: Always provide explicit types when creating schemas:

    ts
    jsonSchema<YourType>({ /* ... */ })
  2. Error Handling: Choose the appropriate validation method:

    • Use validateTypes when validation failures should halt execution
    • Use safeValidateTypes when you need to handle validation errors gracefully
  3. Custom Validation: Combine JSON Schema with custom validators for complex validation rules

  4. Modularity: Create reusable schemas and validators for common patterns

  5. Type Guards: Use type guards when working with dynamic schemas or validators