Architecture

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

  1. Setup Time: Environment variable schemas are validated when the integration is configured
  2. Runtime: Data pin schemas are validated when data flows through the workflow
  3. 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.

On this page