Schema Validation
Schema validation in Xentom integrations provides runtime type safety and data validation using the Standard Schema specification. This ensures data integrity, provides clear error messages, and improves the developer experience with compile-time type inference.
Standard Schema Compliance
Xentom uses the Standard Schema specification, which provides a unified interface for validation libraries. This allows compatibility with popular validation libraries including:
- Valibot - Functional, modular, and type-safe validation
- Zod - TypeScript-first schema validation with static type inference
- ArkType - Runtime validation with compile-time type inference
Core Concepts
Where Schema Validation is Used
Schema validation can be applied to:
- Environment Variables: Validate global configuration during integration setup
- Data Pins: Validate inputs and outputs for individual nodes
- Dynamic Validation: Context-aware validation based on integration state
Validation Lifecycle
- Setup Time: Environment variable schemas are validated when the integration is configured
- Runtime: Data pin schemas are validated when data flows through the workflow
- Error Handling: Validation failures provide clear error messages and prevent execution
Valibot Examples
Valibot provides a functional, composable approach to validation:
Basic Types
import * as v from 'valibot';
// String validation
schema: v.pipe(
v.string(),
v.minLength(1, 'Field is required'),
v.maxLength(100, 'Maximum 100 characters'),
);
// Number validation
schema: v.pipe(
v.number(),
v.integer('Must be a whole number'),
v.minValue(0, 'Must be positive'),
v.maxValue(999, 'Maximum value is 999'),
);
// Boolean validation
schema: v.boolean();
// Email validation
schema: v.pipe(
v.string(),
v.email('Please enter a valid email address'),
v.trim(),
);Complex Types
// Object validation
schema: v.object({
name: v.pipe(v.string(), v.minLength(1)),
age: v.pipe(v.number(), v.integer(), v.minValue(0)),
email: v.pipe(v.string(), v.email()),
isActive: v.boolean(),
tags: v.optional(v.array(v.string())),
});
// Array validation
schema: v.array(
v.object({
id: v.string(),
name: v.string(),
price: v.pipe(v.number(), v.minValue(0)),
}),
);
// Union types
schema: v.union([
v.literal('development'),
v.literal('staging'),
v.literal('production'),
]);
// Transform and validation
schema: v.pipe(
v.string(),
v.transform(Number),
v.number(),
v.integer(),
v.minValue(1),
v.maxValue(65535),
);Environment Variable Example
import * as v from 'valibot';
import * as i from '@xentom/integration-framework';
export default i.integration({
env: {
API_KEY: i.env({
control: i.controls.text({
label: 'API Key',
sensitive: true,
}),
schema: v.pipe(
v.string(),
v.startsWith('sk-', 'API key must start with "sk-"'),
v.minLength(10, 'API key too short'),
),
}),
RETRY_COUNT: i.env({
control: i.controls.text({
label: 'Retry Attempts',
defaultValue: '3',
}),
schema: v.pipe(
v.string(),
v.transform(Number),
v.number(),
v.integer(),
v.minValue(0),
v.maxValue(10),
),
}),
},
});Zod Examples
Zod provides TypeScript-first validation with excellent type inference:
Basic Types
import { z } from 'zod';
// String validation
schema: z.string()
.min(1, 'Field is required')
.max(100, 'Maximum 100 characters');
// Number validation
schema: z.number()
.int('Must be a whole number')
.min(0, 'Must be positive')
.max(999, 'Maximum value is 999');
// Boolean validation
schema: z.boolean();
// Email validation
schema: z.string().email('Please enter a valid email address').trim();Complex Types
// Object validation
schema: z.object({
name: z.string().min(1),
age: z.number().int().min(0),
email: z.string().email(),
isActive: z.boolean(),
tags: z.array(z.string()).optional(),
});
// Array validation
schema: z.array(
z.object({
id: z.string(),
name: z.string(),
price: z.number().min(0),
}),
);
// Union types
schema: z.enum(['development', 'staging', 'production']);
// Transform and validation
schema: z.string().transform(Number).pipe(z.number().int().min(1).max(65535));Data Pin Example
import { z } from 'zod';
import * as i from '@xentom/integration-framework';
export const createUser = i.nodes.callable({
inputs: {
userData: i.pins.data({
control: i.controls.expression({
label: 'User Data',
}),
schema: z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
age: z.number().int().min(13, 'Must be at least 13 years old'),
preferences: z
.object({
notifications: z.boolean().default(true),
theme: z.enum(['light', 'dark']).default('light'),
})
.optional(),
}),
}),
},
outputs: {
user: i.pins.data({
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
}),
}),
},
async run({ inputs, next }) {
// TypeScript will infer the correct types based on schema
const user = await createUserInDatabase(inputs.userData);
next({ user });
},
});ArkType Examples
ArkType provides runtime validation with TypeScript-like syntax:
Basic Types
import { type } from 'arktype';
// String validation
schema: type('string>0<=100'); // String with length 1-100
// Number validation
schema: type('integer>0<=999'); // Integer between 1 and 999
// Boolean validation
schema: type('boolean');
// Email validation (requires custom validation)
schema: type('string').pipe((email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email)
? email
: type.errors.validationError('Invalid email');
});Complex Types
// Object validation
schema: type({
name: 'string>0',
age: 'integer>=0',
email: 'string',
isActive: 'boolean',
'tags?': 'string[]'
})
// Array validation
schema: type({
id: 'string',
name: 'string',
price: 'number>=0'
}[])
// Union types
schema: type("'development'|'staging'|'production'")
// Complex nested types
schema: type({
user: {
id: 'string',
profile: {
name: 'string>0',
email: 'string',
settings: {
notifications: 'boolean',
theme: "'light'|'dark'"
}
}
},
metadata: {
createdAt: 'string',
version: 'number'
}
})Environment Variable Example
import { type } from 'arktype';
import * as i from '@xentom/integration-framework';
export default i.integration({
env: {
API_URL: i.env({
control: i.controls.text({
label: 'API Base URL',
defaultValue: 'https://api.example.com',
}),
schema: type('string').pipe((url) => {
try {
new URL(url);
return url;
} catch {
return type.errors.validationError('Must be a valid URL');
}
}),
}),
TIMEOUT_MS: i.env({
control: i.controls.text({
label: 'Timeout (milliseconds)',
defaultValue: '30000',
}),
schema: type('string').pipe((str) => {
const num = Number(str);
return type('integer>=1000<=300000').assert(num);
}),
}),
},
});Dynamic Schema Validation
Schemas can be dynamic based on integration context:
Context-Aware Validation
// Using Valibot
i.pins.data({
displayName: 'API Response',
schema: ({ state }) => {
if (state.apiVersion === 'v2') {
return v.object({
data: v.any(),
metadata: v.object({
version: v.literal('v2'),
timestamp: v.string(),
}),
});
} else {
return v.object({
data: v.any(),
success: v.boolean(),
});
}
},
});
// Using Zod
i.pins.data({
displayName: 'Configuration',
schema: ({ state }) => {
const baseSchema = z.object({
name: z.string(),
type: z.enum(['basic', 'advanced']),
});
if (state.userRole === 'admin') {
return baseSchema.extend({
adminSettings: z.object({
permissions: z.array(z.string()),
auditLog: z.boolean(),
}),
});
}
return baseSchema;
},
});Conditional Validation
i.pins.data({
schema: ({ inputs }) => {
// Access other input values for conditional validation
if (inputs.authenticationType === 'oauth2') {
return v.object({
clientId: v.string(),
clientSecret: v.string(),
redirectUri: v.pipe(v.string(), v.url()),
});
} else {
return v.object({
apiKey: v.pipe(v.string(), v.minLength(10)),
});
}
},
});Error Handling
Validation Error Structure
When validation fails, the framework provides detailed error information:
// Valibot error example
{
issues: [
{
input: "invalid-email",
path: [{ key: "email" }],
message: "Please enter a valid email address"
}
]
}
// Zod error example
{
code: "invalid_string",
path: ["email"],
message: "Invalid email format"
}Handling Validation Errors in Nodes
export const processData = i.nodes.callable({
inputs: {
data: i.pins.data({
schema: v.object({
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(0)),
}),
}),
},
outputs: {
success: i.pins.exec({
outputs: {
result: i.pins.data(),
},
}),
validationError: i.pins.exec({
outputs: {
errors: i.pins.data(),
},
}),
},
run({ inputs, next }) {
try {
// If we reach this point, validation has passed
const result = processValidData(inputs.data);
next('success', { result });
} catch (error) {
// This would catch other processing errors, not validation
throw error;
}
},
});Best Practices
Choose the Right Library
- Valibot: Best for functional programming style and maximum flexibility
- Zod: Best for TypeScript projects with excellent type inference
- ArkType: Best for performance-critical applications with minimal bundle size
Schema Design
// Good - Clear, specific validation
schema: v.object({
email: v.pipe(v.string(), v.email('Invalid email format')),
password: v.pipe(
v.string(),
v.minLength(8, 'Password must be at least 8 characters'),
v.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain uppercase, lowercase, and number',
),
),
});
// Avoid - Vague validation
schema: v.object({
email: v.string(), // No format validation
password: v.string(), // No strength requirements
});Error Messages
// Good - Helpful error messages
schema: v.pipe(
v.string(),
v.minLength(1, 'This field is required'),
v.maxLength(50, 'Maximum 50 characters allowed'),
v.regex(/^[a-zA-Z\s]+$/, 'Only letters and spaces are allowed'),
);
// Avoid - Generic messages
schema: v.pipe(
v.string(),
v.minLength(1), // Uses default message
v.maxLength(50), // Uses default message
);Performance Considerations
// Good - Cache complex schemas
const userSchema = v.object({
// ... complex validation
});
// Reuse the cached schema
i.pins.data({ schema: userSchema });
// Avoid - Recreating schemas
i.pins.data({
schema: v.object({
// Recreating the same complex schema repeatedly
}),
});Environment vs Runtime Validation
// Environment variables - Simple, static validation
env: {
API_KEY: i.env({
schema: v.pipe(v.string(), v.minLength(10)),
});
}
// Data pins - Can be complex and dynamic
inputs: {
userData: i.pins.data({
schema: ({ state }) => getDynamicUserSchema(state.userRole),
});
}Schema validation is a powerful feature that ensures data integrity throughout your Xentom integrations while providing excellent developer experience with type safety and clear error messages.