Skip to content

Molinit/validator

Repository files navigation

Validator

A flexible, type-safe validation library for TypeScript/JavaScript that provides declarative schema-based validation with support for complex data structures, conditional requirements, and custom validators.

Table of Contents


Overview

Validator is designed to validate complex nested data structures using a declarative schema approach. Instead of writing imperative validation code, you define a schema that describes your data requirements, and the validator handles the rest.

Key Benefits

  • Declarative: Define validation rules as data structures
  • Type-safe: Full TypeScript support with strict typing
  • Composable: Build complex validations from simple rules
  • Extensible: Add custom validation functions
  • Conditional: Support for dependent field validation
  • Comprehensive: Validates primitives, objects, arrays, and nested structures

Features

  • Primitive validation: strings, numbers, booleans
  • Pattern matching: regex validation
  • Length constraints: min/max length validation
  • Enum validation: one-of allowed values
  • Array validation: validate arrays and their items
  • Object shape validation: validate object structures with type checking
  • Array of objects: validate arrays of structured objects
  • Conditional validation: fields required based on other fields
  • Custom validators: add your own validation logic
  • Comprehensive error messages: clear, actionable error reporting

Installation

npm install @molinit/validator

Or with yarn:

yarn add @molinit/validator

Quick Start

import validate from '@molinit/validator';
import { ValidationDescription } from '@molinit/validator/types';

// Define your validation schema
const userSchema: ValidationDescription = {
  email: {
    required: true,
    regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
  age: {
    required: true,
    minLength: 1,
    maxLength: 3,
  },
  role: {
    required: true,
    oneof: ['admin', 'user', 'guest'],
  },
};

// Validate data
const userData = {
  email: 'user@example.com',
  age: '25',
  role: 'user',
};

const errors = validate(userData, userSchema);

if (errors.length > 0) {
  console.error('Validation failed:', errors);
  // [{ field: 'email', message: 'Missing required field' }]
} else {
  console.log('Validation passed!');
}

Core Concepts

Validation Schema

A validation schema is a plain JavaScript object that describes the validation rules for your data:

const schema: ValidationDescription = {
  fieldName: {
    // Validation rules go here
    required: true,
    minLength: 5,
    maxLength: 100,
  },
};

Validation Rules

Each field in your schema can have multiple validation rules. All rules for a field must pass for the data to be valid.

Error Format

Validation errors are returned as an array of objects:

type ValidationError = {
  field: string; // The field that failed validation
  message: string; // Human-readable error message
};

Validation Rules

required

Ensures a field is present and not empty.

{
  username: {
    required: true,
  }
}

Validates:

  • Field exists
  • Value is not undefined, null, or empty string
  • For arrays: at least one item exists (when used with array: true)

required_depends_on

Makes a field required only when another field has a specific value.

{
  password_confirmation: {
    required_depends_on: { key: 'change_password', value: '1' },
  }
}

Example:

// Valid: change_password is '0', so confirmation not required
{ change_password: '0', password_confirmation: '' }

// Invalid: change_password is '1', so confirmation is required
{ change_password: '1', password_confirmation: '' }

minLength / maxLength

Validates the length of a string or the string representation of a value.

{
  username: {
    minLength: 3,
    maxLength: 20,
  }
}

Example:

{
  username: 'ab';
} // Invalid: too short
{
  username: 'alice';
} // Valid
{
  username: 'a'.repeat(21);
} // Invalid: too long

regexp

Validates a field against a regular expression.

{
  email: {
    regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
  zipCode: {
    regexp: /^\d{5}(-\d{4})?$/,
  }
}

Example:

{
  email: 'user@example.com';
} // Valid
{
  email: 'invalid-email';
} // Invalid

equal

Validates that a field equals a specific value.

{
  confirmation: {
    equal: 'I agree to the terms',
  }
}

oneof

Validates that a field is one of the allowed values (enum validation).

{
  status: {
    oneof: ['draft', 'published', 'archived'],
  },
  country: {
    oneof: ['US', 'CA', 'MX', 'UK', 'FR', 'DE'],
  }
}

Example:

{
  status: 'published';
} // Valid
{
  status: 'pending';
} // Invalid

array

Validates that a field is an array.

{
  tags: {
    required: true,
    array: true,  // Must be an array with at least one item
  }
}

Example:

{
  tags: ['javascript', 'typescript'];
} // Valid
{
  tags: [];
} // Invalid: empty array with required: true
{
  tags: 'string';
} // Invalid: not an array

array_items

Validates that all items in an array are from an allowed list.

{
  tags: {
    array: true,
    array_items: ['javascript', 'typescript', 'python', 'rust'],
  }
}

Example:

{
  tags: ['javascript', 'typescript'];
} // Valid
{
  tags: ['javascript', 'php'];
} // Invalid: 'php' not in allowed list

Supports:

  • Primitives (strings, numbers, booleans)
  • Objects (deep comparison via JSON)

shapeOf

Validates that a field is an object matching a specific shape with optional custom validators.

{
  address: {
    shapeOf: {
      shape: {
        street: 'string',
        city: 'string',
        zipCode: 'string',
        country: ['US', 'CA', 'MX'],  // Enum validation
      },
      required: ['street', 'city', 'country'],
      validators: {
        zipCode: (value) => /^\d{5}$/.test(value as string),
      },
    },
  }
}

Features:

  • Type checking: Validates each property type
  • Required properties: Specify which properties are mandatory
  • Enum values: Use array for allowed values
  • Custom validators: Add property-specific validation functions

Example:

// Valid
{
  address: {
    street: '123 Main St',
    city: 'New York',
    zipCode: '10001',
    country: 'US',
  }
}

// Invalid: missing required 'city'
{
  address: {
    street: '123 Main St',
    country: 'US',
  }
}

// Invalid: wrong type for 'zipCode'
{
  address: {
    street: '123 Main St',
    city: 'New York',
    zipCode: 12345,  // Should be string
    country: 'US',
  }
}

arrayOfShapes

Validates an array of objects, each matching a specific shape.

{
  items: {
    arrayOfShapes: {
      shape: {
        name: 'string',
        quantity: 'number',
        price: 'number',
        category: ['electronics', 'clothing', 'food'],
      },
      required: ['name', 'quantity', 'price'],
      validators: {
        _array: (arr) => arr.length <= 100,  // Array-level validator
        price: (price) => price > 0,         // Property-level validator
        quantity: (qty) => qty > 0 && qty <= 1000,
      },
    },
  }
}

Features:

  • Array-level validation: _array validator for the entire array
  • Property-level validation: Individual validators for each property
  • Type checking: Each object property validated
  • Required properties: Per-object required fields

Example:

// Valid
{
  items: [
    { name: 'Laptop', quantity: 2, price: 999.99, category: 'electronics' },
    { name: 'T-Shirt', quantity: 5, price: 19.99, category: 'clothing' },
  ];
}

// Invalid: second item has negative price
{
  items: [
    { name: 'Laptop', quantity: 2, price: 999.99, category: 'electronics' },
    { name: 'T-Shirt', quantity: 5, price: -19.99, category: 'clothing' },
  ];
}

at_least_one_of

Validates that at least one field from a group has a value. Useful when you have multiple optional fields but require at least one to be filled.

{
  contact_email: {
    at_least_one_of: {
      fields: ['contact_email', 'contact_phone', 'contact_address'],
    },
  },
  contact_phone: {
    at_least_one_of: {
      fields: ['contact_email', 'contact_phone', 'contact_address'],
    },
  },
  contact_address: {
    at_least_one_of: {
      fields: ['contact_email', 'contact_phone', 'contact_address'],
    },
  },
}

Features:

  • Group validation: Checks if ANY field in the group has a value
  • Conditional requirement: Optional required_depends_on for conditional validation
  • Flexible: Works with strings, numbers, booleans, and arrays
  • Empty detection: Treats '', null, undefined, and [] as empty

Example:

// Invalid: None of the contact fields have values
{
  contact_email: '',
  contact_phone: '',
  contact_address: '',
}
// Error on all three fields: "At least one must be selected"

// Valid: At least one contact method provided
{
  contact_email: 'user@example.com',
  contact_phone: '',
  contact_address: '',
}

With Conditional Requirement:

{
  social_facebook: {
    at_least_one_of: {
      fields: ['social_facebook', 'social_twitter', 'social_instagram'],
      required_depends_on: { key: 'has_social', value: 'yes' },
    },
  },
  // Same config for social_twitter and social_instagram
}

When has_social = 'yes', at least one social media field must have a value.


Advanced Usage

Conditional Validation

Create fields that are only required when certain conditions are met:

const checkoutSchema: ValidationDescription = {
  shipping_method: {
    required: true,
    oneof: ['standard', 'express', 'pickup'],
  },
  shipping_address: {
    required_depends_on: { key: 'shipping_method', value: 'standard' },
    shapeOf: {
      shape: {
        street: 'string',
        city: 'string',
        state: 'string',
        zip: 'string',
      },
      required: ['street', 'city', 'state', 'zip'],
    },
  },
  pickup_location: {
    required_depends_on: { key: 'shipping_method', value: 'pickup' },
    oneof: ['store_1', 'store_2', 'store_3'],
  },
};

Nested Object Validation

Validate complex nested structures:

const orderSchema: ValidationDescription = {
  customer: {
    required: true,
    shapeOf: {
      shape: {
        name: 'string',
        email: 'string',
        phone: 'string',
      },
      required: ['name', 'email'],
      validators: {
        email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email as string),
        phone: (phone) => /^\d{10}$/.test(phone as string),
      },
    },
  },
  items: {
    required: true,
    arrayOfShapes: {
      shape: {
        product_id: 'string',
        quantity: 'number',
        price: 'number',
      },
      required: ['product_id', 'quantity', 'price'],
      validators: {
        _array: (items) => items.length > 0 && items.length <= 50,
        quantity: (qty) => qty > 0 && qty <= 100,
        price: (price) => price >= 0,
      },
    },
  },
  total: {
    required: true,
    regexp: /^\d+\.\d{2}$/,
  },
};

Custom Validators

Create reusable custom validation functions:

// Custom validators
const isValidUUID = (value: unknown): boolean => {
  const uuidRegex =
    /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
  return uuidRegex.test(value as string);
};

const isValidBarcode = (value: unknown): boolean => {
  const code = value as string;
  return /^\d{8,14}$/.test(code) && hasValidChecksum(code);
};

const isStrongPassword = (value: unknown): boolean => {
  const password = value as string;
  return (
    password.length >= 8 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password) &&
    /[^A-Za-z0-9]/.test(password)
  );
};

// Use in schema
const userSchema: ValidationDescription = {
  id: {
    required: true,
    shapeOf: {
      shape: { uuid: 'string' },
      required: ['uuid'],
      validators: { uuid: isValidUUID },
    },
  },
  password: {
    required: true,
    shapeOf: {
      shape: { value: 'string' },
      required: ['value'],
      validators: { value: isStrongPassword },
    },
  },
};

Multiple Type Options

Validate fields that can be multiple types:

{
  value: {
    shapeOf: {
      shape: {
        data: ['string', 'number'],  // Can be string OR number
      },
      required: ['data'],
    },
  }
}

API Reference

Main Function

validate(input, schema)

Validates input data against a schema.

Parameters:

  • input: ValidationInput - The data to validate (object with string keys)
  • schema: ValidationDescription - The validation schema

Returns:

  • ValidationError[] - Array of validation errors (empty if valid)

Example:

const errors = validate(userData, userSchema);
if (errors.length === 0) {
  // Data is valid
}

Validator Class

You can also use the Validator class directly for more control:

import Validator from '@molinit/validator/Validator';

const validator = new Validator();

// Validate individual fields
const error = validator.validate_required('email', '', true, {});
// Returns: { field: 'email', message: 'Missing required field' }

const error2 = validator.validate_regexp(
  'email',
  'invalid',
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  {},
);
// Returns: { field: 'email', message: "Field doesn't match the required pattern" }

Available Methods

  • validate_required(field, value, options, input)
  • validate_required_depends_on(field, value, options, input)
  • validate_minLength(field, value, options, input)
  • validate_maxLength(field, value, options, input)
  • validate_regexp(field, value, options, input)
  • validate_equal(field, value, options, input)
  • validate_oneof(field, value, options, input)
  • validate_array(field, value, options, input)
  • validate_array_items(field, value, options, input)
  • validate_shapeOf(field, value, options, input)
  • validate_arrayOfShapes(field, value, options, input)
  • validate_at_least_one_of(field, value, options, input)

Examples

Basic User Registration

const registrationSchema: ValidationDescription = {
  username: {
    required: true,
    minLength: 3,
    maxLength: 20,
    regexp: /^[a-zA-Z0-9_]+$/,
  },
  email: {
    required: true,
    regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
  password: {
    required: true,
    minLength: 8,
  },
  terms_accepted: {
    required: true,
    equal: 'yes',
  },
};

const userData = {
  username: 'alice_2024',
  email: 'alice@example.com',
  password: 'SecurePass123!',
  terms_accepted: 'yes',
};

const errors = validate(userData, registrationSchema);
// errors = [] (valid)

E-commerce Product

const productSchema: ValidationDescription = {
  sku: {
    required: true,
    regexp: /^[A-Z]{3}-\d{4}$/,
  },
  name: {
    required: true,
    minLength: 2,
    maxLength: 100,
  },
  category: {
    required: true,
    oneof: ['electronics', 'clothing', 'food', 'books'],
  },
  price: {
    required: true,
    regexp: /^\d+\.\d{2}$/,
  },
  tags: {
    array: true,
    array_items: ['new', 'sale', 'featured', 'popular', 'clearance'],
  },
  inventory: {
    required: true,
    shapeOf: {
      shape: {
        quantity: 'number',
        location: 'string',
        restock_date: 'string',
      },
      required: ['quantity', 'location'],
      validators: {
        quantity: (qty) => (qty as number) >= 0,
      },
    },
  },
};

Wine Listing (Real-World Example)

const wineListing Schema: ValidationDescription = {
  name: {
    required: true,
    minLength: 2,
    maxLength: 255,
  },
  vintage: {
    required: true,
    regexp: /^\d{4}$/,
  },
  wine_type: {
    required: true,
    oneof: ['red', 'white', 'rose', 'sparkling', 'dessert'],
  },
  origin_country: {
    required: true,
    regexp: /^[A-Z]{2}$/,  // ISO country codes
  },
  grapes: {
    required: true,
    arrayOfShapes: {
      shape: {
        name: 'string',
        percentage: 'string',
      },
      required: ['name', 'percentage'],
      validators: {
        _array: (grapes) => {
          // Percentages must sum to 100
          const total = grapes.reduce((sum, g) => sum + parseInt(g.percentage), 0);
          return total === 100;
        },
        percentage: (pct) => {
          const num = parseInt(pct as string);
          return num >= 0 && num <= 100;
        },
      },
    },
  },
  barcode: {
    required_depends_on: { key: 'require_barcode', value: '1' },
    shapeOf: {
      shape: {
        code: 'string',
        type: ['EAN8', 'EAN13', 'UPC', 'GTIN14', 'unknown'],
        normalized_gtin: 'string',
      },
      validators: {
        code: (code) => /^\d{8,14}$/.test(code as string),
      },
      required: ['code', 'type'],
    },
  },
};

TypeScript Support

Full Type Definitions

import {
  ValidationDescription,
  ValidationAttributes,
  ValidationError,
  ValidationInput,
  ShapeOfValidation,
  ArrayOfShapesValidation,
} from '@molinit/validator/types';

// Define typed schemas
const schema: ValidationDescription = {
  // Your schema here
};

// Type-safe error handling
const errors: ValidationError[] = validate(input, schema);

// Custom validators are type-safe
const customValidator = (value: unknown): boolean => {
  // Your validation logic
  return true;
};

Type Inference

The validator provides excellent type inference for your schemas:

const schema = {
  email: {
    required: true,
    regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
} satisfies ValidationDescription;

// TypeScript knows all available validation rules

Best Practices

1. Define Schemas as Constants

// ✅ Good: Reusable schema
export const USER_SCHEMA: ValidationDescription = {
  email: { required: true, regexp: /.../ },
};

// ❌ Bad: Inline schema
const errors = validate(data, { email: { required: true } });

2. Use Custom Validators for Complex Logic

// ✅ Good: Reusable, testable validator
const isValidCreditCard = (value: unknown): boolean => {
  const num = value as string;
  return luhnCheck(num) && num.length >= 13;
};

// ❌ Bad: Complex regex
regexp: /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|...)$/;

3. Group Related Schemas

// schemas/user.ts
export const USER_SCHEMA = {
  /* ... */
};
export const USER_PROFILE_SCHEMA = {
  /* ... */
};

// schemas/product.ts
export const PRODUCT_SCHEMA = {
  /* ... */
};
export const PRODUCT_VARIANT_SCHEMA = {
  /* ... */
};

4. Provide Clear Error Messages

The validator provides default error messages, but you can wrap it for custom messages:

const errors = validate(data, schema);
if (errors.length > 0) {
  const customErrors = errors.map((err) => ({
    ...err,
    message: getCustomMessage(err.field, err.message),
  }));
}

5. Test Your Schemas

describe('User Schema', () => {
  it('should validate valid user data', () => {
    const validUser = { email: 'test@example.com', age: '25' };
    const errors = validate(validUser, USER_SCHEMA);
    expect(errors).toHaveLength(0);
  });

  it('should reject invalid email', () => {
    const invalidUser = { email: 'invalid', age: '25' };
    const errors = validate(invalidUser, USER_SCHEMA);
    expect(errors).toContainEqual({
      field: 'email',
      message: expect.stringContaining('pattern'),
    });
  });
});

Common Patterns

Optional Fields with Constraints

{
  bio: {
    required: false,
    minLength: 10,    // If provided, must be at least 10 chars
    maxLength: 500,   // And at most 500 chars
  }
}

Mutually Exclusive Fields

{
  email: {
    required_depends_on: { key: 'contact_method', value: 'email' },
    regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
  phone: {
    required_depends_on: { key: 'contact_method', value: 'phone' },
    regexp: /^\d{10}$/,
  },
}

Percentage Validation

const grapes = {
  arrayOfShapes: {
    shape: {
      name: 'string',
      percentage: 'number',
    },
    required: ['name', 'percentage'],
    validators: {
      _array: (items) => {
        const total = items.reduce((sum, item) => sum + item.percentage, 0);
        return total === 100;
      },
      percentage: (pct) => pct >= 0 && pct <= 100,
    },
  },
};

Error Handling

Accessing Errors

const errors = validate(data, schema);

// Check if valid
if (errors.length === 0) {
  console.log('Valid!');
}

// Get specific field errors
const emailErrors = errors.filter((e) => e.field === 'email');

// Group errors by field
const errorsByField = errors.reduce(
  (acc, err) => {
    if (!acc[err.field]) acc[err.field] = [];
    acc[err.field].push(err.message);
    return acc;
  },
  {} as Record<string, string[]>,
);

Custom Error Handling

function validateWithCustomErrors(data: any, schema: ValidationDescription) {
  const errors = validate(data, schema);

  return errors.map((err) => ({
    field: err.field,
    message: err.message,
    code: getErrorCode(err.message),
    severity: getSeverity(err.field),
  }));
}

Performance Tips

  1. Reuse Validator Instance: The validator is stateless and can be reused
  2. Cache Schemas: Define schemas once as constants
  3. Lazy Validation: Only validate fields that changed
  4. Optimize Custom Validators: Keep custom validators fast and simple

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

Development Setup

git clone https://github.com/molinit/validator
cd validator
npm install
npm test

Running Tests

npm test                 # Run all tests
npm test -- --watch      # Watch mode
npm test -- --coverage   # With coverage

License

MIT License - see LICENSE file for details.


Support


Changelog

See CHANGELOG.md for version history and updates.


Made with ❤️ by the Molinit team

About

A flexible type-safe validation library for TypeScript/JavaScript

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published