JSPM

@molinit/validator

1.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 4
  • Score
    100M100P100Q33473F
  • License MIT

A flexible, type-safe validation library for TypeScript/JavaScript with declarative schema-based validation

Package Exports

  • @molinit/validator
  • @molinit/validator/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@molinit/validator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

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}|...)$/;
// 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