Schema Adapters

Schema adapters for request/response validation and OpenAPI generation. Supports Zod and TypeBox.

Overview

Schema adapters wrap validation libraries (Zod, TypeBox) with a consistent interface for:

  • Request/response validation in route definitions
  • JSON Schema generation for OpenAPI documentation
  • Full TypeScript type inference

Available adapters:

  • Zod - TypeScript-first schema validation (zodSchema)
  • TypeBox - JSON Schema Type Builder (typeboxSchema)

Schema Adapter Interface

All adapters implement SchemaAdapter<TOutput, TInput>:

interface SchemaAdapter<TOutput, TInput = TOutput> {
  validate(data: unknown): ValidationResult<TOutput>;
  toJsonSchema(options?: JsonSchemaOptions): JsonSchema;
  _output: TOutput;  // Phantom type for inference
  _input: TInput;    // Phantom type for inference
}

type ValidationResult<T> =
  | { success: true; data: T }
  | { success: false; issues: ValidationIssue[] };

Zod Adapter

The Zod adapter wraps Zod schemas for use in route definitions:

import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';

// Create a schema adapter
const userSchema = zodSchema(
  z.object({
    id: z.string().uuid(),
    email: z.string().email(),
    name: z.string().min(1).max(100),
    age: z.number().int().min(0).optional(),
  }),
);

// Use in route definition
import { defineRoute } from '@cosmneo/onion-lasagna/http/route';

export const getUserRoute = defineRoute({
  method: 'GET',
  path: '/api/users/:userId',
  responses: {
    200: {
      description: 'User found',
      schema: userSchema,
    },
  },
});

TypeBox Adapter

TypeBox schemas ARE JSON Schema, making them ideal for OpenAPI generation:

import { typeboxSchema, Type } from '@cosmneo/onion-lasagna/http/schema/typebox';

// Create a schema adapter
const userSchema = typeboxSchema(
  Type.Object({
    id: Type.String({ format: 'uuid' }),
    email: Type.String({ format: 'email' }),
    name: Type.String({ minLength: 1, maxLength: 100 }),
    age: Type.Optional(Type.Integer({ minimum: 0 })),
  }),
);

// Use in route definition
export const getUserRoute = defineRoute({
  method: 'GET',
  path: '/api/users/:userId',
  responses: {
    200: {
      description: 'User found',
      schema: userSchema,
    },
  },
});

Using in Route Definitions

Schema adapters are used in route definitions for request and response validation:

import { defineRoute } from '@cosmneo/onion-lasagna/http/route';
import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';

export const createUserRoute = defineRoute({
  method: 'POST',
  path: '/api/users',
  request: {
    // Request body validation
    body: {
      schema: zodSchema(
        z.object({
          email: z.string().email(),
          name: z.string().min(1),
          password: z.string().min(8),
        }),
      ),
    },
    // Query parameter validation
    query: {
      schema: zodSchema(
        z.object({
          sendWelcomeEmail: z.coerce.boolean().optional(),
        }),
      ),
    },
    // Path parameter validation
    params: {
      schema: zodSchema(
        z.object({
          orgId: z.string().uuid(),
        }),
      ),
    },
    // Context validation (from middleware)
    context: {
      schema: zodSchema(
        z.object({
          userId: z.string(),
        }),
      ),
    },
  },
  responses: {
    201: {
      description: 'User created',
      schema: zodSchema(
        z.object({
          userId: z.string().uuid(),
        }),
      ),
    },
    400: {
      description: 'Validation error',
    },
  },
});

Automatic Validation

When you register routes with a framework adapter, request validation happens automatically:

import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';

const routes = serverRoutes(userRouter)
  .handle('users.create', {
    // req.body, req.query, req.pathParams are already validated and typed
    requestMapper: (req, ctx) => ({
      email: req.body.email,      // string (validated)
      name: req.body.name,        // string (validated)
      createdBy: ctx.userId,      // string (from context schema)
    }),
    useCase: createUserUseCase,
    responseMapper: (output) => ({
      status: 201 as const,
      body: { userId: output.userId },
    }),
  })
  .build();

If validation fails, a 400 response is returned with structured errors:

{
  "errorCode": "VALIDATION_ERROR",
  "message": "Request validation failed",
  "details": [
    { "path": ["body", "email"], "message": "Invalid email" }
  ]
}

JSON Schema Generation

Schema adapters can generate JSON Schema for OpenAPI documentation:

const userSchema = zodSchema(
  z.object({
    name: z.string().describe('User full name'),
    email: z.string().email().describe('User email address'),
  }),
);

// Get JSON Schema
const jsonSchema = userSchema.toJsonSchema();
// {
//   type: 'object',
//   properties: {
//     name: { type: 'string', description: 'User full name' },
//     email: { type: 'string', format: 'email', description: 'User email address' },
//   },
//   required: ['name', 'email'],
// }

Use generateOpenAPI() to create a full OpenAPI spec from your router:

import { generateOpenAPI } from '@cosmneo/onion-lasagna/http/openapi';

const spec = generateOpenAPI(userRouter, {
  info: { title: 'User API', version: '1.0.0' },
  servers: [{ url: 'https://api.example.com' }],
});

Type Inference

Schema adapters provide full type inference:

import type { InferOutput, InferInput } from '@cosmneo/onion-lasagna/http';

const userSchema = zodSchema(
  z.object({
    name: z.string(),
    age: z.number(),
  }),
);

// Infer types from schema
type User = InferOutput<typeof userSchema>;
// { name: string; age: number }

Types are automatically inferred in handlers:

.handle('users.create', {
  requestMapper: (req, ctx) => {
    // req.body is typed based on the route's body schema
    const email: string = req.body.email;  // TypeScript knows this is string
    return { email, createdBy: ctx.userId };
  },
  // ...
})

Value Objects vs Schema Adapters

AspectSchema AdaptersValue Objects
PurposeBoundary validationDomain invariants
LocationRoute definitions (presentation)Domain layer
ValidationSchema-based (JSON Schema compatible)Plain TypeScript
When to useHTTP request/response validationDomain concepts

Schema adapters validate untrusted input at system boundaries:

// Route definition with schema validation
const createUserRoute = defineRoute({
  request: {
    body: { schema: zodSchema(z.object({ email: z.string().email() })) },
  },
});

Value Objects enforce domain invariants:

// Domain validation with business rules
class Email extends BaseValueObject<string> {
  static create(value: string): Email {
    if (!Email.isValidEmail(value)) {
      throw new InvariantViolationError({ message: 'Invalid email' });
    }
    return new Email(value);
  }
}

Adapter Comparison

FeatureZodTypeBox
Bundle size~12kb~15kb
Type inferenceExcellentGood
Error messagesDetailed, customizableBasic
JSON SchemaConversion requiredNative (zero-cost)
Best forGeneral useOpenAPI-first APIs

Choose Zod for:

  • Better error messages
  • More validation features (refinements, transforms)
  • Familiar API

Choose TypeBox for:

  • Native JSON Schema (faster OpenAPI generation)
  • Better performance
  • Smaller runtime overhead