Schema & Validation

The schema module (@putnami/runtime) provides type-safe schema definitions with compile-time inference and runtime validation. Schemas are used across the framework for endpoint bodies, query parameters, configuration, and domain models.

Defining a Schema

Use native constructors (String, Number, Boolean) and schema helpers to describe the shape of your data:

import { schema, Optional, Int, Email, OneOf, ArrayOf } from '@putnami/runtime';

const TaskSchema = schema({
  title: String,
  description: Optional(String),
  priority: Int,
  status: OneOf('todo', 'in_progress', 'done'),
  assignees: ArrayOf(Email),
});

The schema() helper preserves full type inference — InferSchema<typeof TaskSchema> produces the equivalent TypeScript type automatically.

Primitives

Schema TypeScript type Validation
String string Must be a string
Number number Must be a number
Boolean boolean Must be a boolean

Built-in Types

Schema TypeScript type Validation
Uuid string UUID v4 format
Email string Basic email format
Int number Integer (no decimals)
Url string Valid URL
DateIso string ISO 8601 date (2024-01-15 or 2024-01-15T10:30:00Z)

Constraints

Numeric and string constraints restrict values beyond their base type:

import { Min, Max, MinLength, MaxLength, Pattern, OneOf, Constrained } from '@putnami/runtime';

const PasswordSchema = schema({
  username: MinLength(3),
  password: Constrained(MinLength(8), MaxLength(128)),
  age: Constrained(Min(0), Max(150)),
  role: OneOf('admin', 'editor', 'viewer'),
  code: Pattern(/^[A-Z]{3}-\d{4}$/),
});
Constraint Applies to Description
Min(n) number Value must be >= n
Max(n) number Value must be <= n
MinLength(n) string Length must be >= n
MaxLength(n) string Length must be <= n
Pattern(regex) string Must match the regex
OneOf(...values) string Must be one of the listed values
Constrained(...descriptors) any Combine multiple constraints

Collections

import { ArrayOf, MapOf, Optional } from '@putnami/runtime';

const Schema = schema({
  tags: ArrayOf(String),                // string[]
  scores: ArrayOf(Int),                 // number[]
  metadata: MapOf(String, String),      // Record<string, string>
  optionalList: Optional(ArrayOf(Uuid)),// string[] | undefined
});

MapOf(keyType, valueType) accepts scalar key types (String, Number, Boolean, Int).

Nested Objects

Schemas can contain nested object schemas:

const AddressSchema = schema({
  street: String,
  city: String,
  zip: Pattern(/^\d{5}$/),
});

const UserSchema = schema({
  name: String,
  address: AddressSchema,
});

Optional & Defaults

import { Optional, Default } from '@putnami/runtime';

const ConfigSchema = schema({
  host: Default(String, 'localhost'),  // string (defaults to 'localhost')
  port: Default(Number, 3000),         // number (defaults to 3000)
  debug: Optional(Boolean),            // boolean | undefined
});

Default makes the field optional in input but always present in the validated output.

Environment & Secrets

For configuration schemas, bind values to environment variables or async resolvers:

import { Env, Resolve, Sensitive } from '@putnami/runtime';

const DbConfig = schema({
  host: Env('DB_HOST', String),
  port: Env('DB_PORT', Int),
  password: Sensitive(Env('DB_PASSWORD', String)),
  token: Resolve(() => secretManager.getSecret('db-token'), String),
});
Helper Purpose
Env(varName, type) Read from environment variable
Resolve(fn, type) Resolve asynchronously at bootstrap
Sensitive(type) Redact value in validation errors
Desc(text, type) Add description for documentation / OpenAPI

Runtime Validation

Use validateSchema() to validate data at runtime:

import { validateSchema } from '@putnami/runtime';

const result = validateSchema(TaskSchema, requestBody);

if (result.errors.length > 0) {
  // result.errors: Array<{ field: string, message: string }>
  return new Response(JSON.stringify({ errors: result.errors }), { status: 400 });
}

// result.data is validated and typed
const task = result.data;

Coercion

When validating URL parameters or query strings (which are always strings), enable coercion:

const result = validateSchema(MySchema, queryParams, { coerce: true });

With coerce: true, string values are automatically converted to numbers and booleans where the schema expects them.

Framework Integration

Schemas are used directly in endpoint definitions — validation happens automatically:

import { endpoint } from '@putnami/application';
import { Int, OneOf, Optional } from '@putnami/runtime';

export const PUT = endpoint()
  .params({ id: String })
  .body({
    title: Optional(String),
    status: Optional(OneOf('todo', 'in_progress', 'done')),
    priority: Optional(Int),
  })
  .handle(async (ctx) => {
    const body = await ctx.body(); // fully typed and validated
    // body.status is 'todo' | 'in_progress' | 'done' | undefined
  });

The same schema types are used for:

  • Endpoint params, query, and body — validated on each request
  • Configuration (useConfig) — validated at startup
  • Domain models — shared type definitions with runtime checks