LIVR Validator

repository·master·Indexed 41 days ago

https://github.com/koorchik/js-validator-livr

A lightweight, fast, and language-independent validation library for JavaScript and TypeScript supporting the Language Independent Validation Rules Specification. Version 2.10.2 provides synchronous and asynchronous validation, full TypeScript type inference via InferFromSchema, data sanitization, and readable error codes. It features built-in rules for strings, numbers, and special formats, as well as support for nested objects and arrays.

Tokens
18.8K
Snippets
51
Records
62
Agent score
50%

What's inside LIVR

  1. Separate validation logic from error messages

    master

    LIVR enforces a clean separation of concerns between the validation layer and the presentation layer. Instead of embedding human-readable strings directly in your validator configuration, you should use Error Codes (e.g., WRONG_EMAIL, TOO_SHORT).

    This allows:

    • Backend independence: The API returns codes, letting web, mobile, or third-party clients decide how to display them.
    • Easy localization: Update translation files (e.g., es.json) without changing application code.
    • Client-specific formatting: Different clients can show different message styles for the same error code.
  2. Compare LIVR with other Validators

    master

    LIVR is designed as a schema-as-data validator, making it uniquely suited for cross-language environments and high security.

    Key Differentiators:

    • Schema as Data: Schemas are pure JSON, allowing them to be stored in databases or shared across languages (Python, Java, etc.) without code execution risks.
    • Error Codes: Instead of plain text messages, LIVR uses semantic error codes, making it easier to implement internationalization (i18n).
    • Automatic Coercion & Stripping: Handles type conversion and removes unknown fields by default.
    • Zero Dependencies: Extremely lightweight (approx. 4.5 KB gzip) and secure.
    | Feature | LIVR | Zod | Joi | fastest-validator | Valibot |
    | :--- | :--- | :--- | :--- | :--- | :--- |
    | **TypeScript Inference** | ✅ `InferFromSchema<>` | ✅ `z.infer<>` | ❌ No | ❌ No | ✅ `InferOutput<>` |
    | **Async Validation** | ✅ AsyncValidator | ⚠️ Via parseAsync | ✅ Yes | ✅ Yes | ⚠️ Via pipeAsync |
    | **Custom Rules** | ✅ Very Easy | ✅ `.refine()` | ✅ `.custom()` | ✅ Yes | ✅ `v.custom()` |
    | **Rules Aliasing** | ✅ `registerAliasedRule()` | ❌ No | ❌ No | ⚠️ Partial | ❌ No |
    | **Schema Serializable** | ✅ Pure JSON | ❌ Code | ❌ Code | ⚠️ Partial | ❌ Code |
    | **Multi-Language Support** | ✅ 10+ languages | ❌ JS/TS only | ❌ JS only | ❌ JS only | ❌ JS/TS only |
    | **Error Format** | Codes (i18n-ready) | Messages | Messages | Messages | Messages |
    | **Error Structure** | Mirrors input | Flat array | Flat array | Flat array | Flat array |
    | **Type Coercion** | ✅ Automatic | Manual | ✅ Via options | ✅ Yes | Manual |
    | **Unknown Fields** | Strip (default) | Strip (default) | Pass through | Pass through | Strip (default) |
    | **Input Immutability** | ✅ Always | ✅ Always | ✅ Always | ⚠️ Mutates | ✅ Always |
    | **Bundle Size (gzip)** | 4.5 KB | 14 KB | 55 KB | 12.4 KB | 1.8 KB |
    | **Zero Dependencies** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
    | **Safe with Untrusted Schemas** | ✅ Yes | N/A | N/A | ❌ RCE risk | N/A |
  3. Handle errors using LIVR's mirrored error structure

    master

    LIVR returns error codes in a structure that mirrors the input data structure. Instead of a flat array of error objects with paths, LIVR provides an object where you can access errors using the same property paths as your input data.

    Key Benefits:

    • Easy Field Mapping: You can access errors directly (e.g., errors.user?.email) rather than searching through an array.
    • Domain-Focused i18n: LIVR returns semantic error codes (e.g., WRONG_EMAIL) rather than implementation-specific messages. This allows the UI layer to handle internationalization (i18n) by mapping these codes to localized strings in a separate translation table.
    // Input structure
    const input = {
        user: {
            email: 'not-an-email',
            profile: {
                age: 'not-a-number',
            },
        },
    };
    
    // LIVR error output (mirrors input)
    // {
    //   user: {
    //     email: 'WRONG_EMAIL',
    //     profile: {
    //       age: 'NOT_INTEGER'
    //     }
    //   }
    // }
    
    // Direct property access for UI mapping
    const emailError = errors.user?.email; // 'WRONG_EMAIL'
    const ageError = errors.user?.profile?.age; // 'NOT_INTEGER'
  4. Understand LIVR's Schema-as-Data design

    master

    LIVR uses pure JSON data structures for schemas instead of fluent APIs or method chaining. This design choice prioritizes portability and composability, allowing schemas to be easily serialized, stored in databases, sent over APIs, or embedded within larger configuration files (like Form Builders, API Gateways, or CMS content types).

    Unlike code-based validators (e.g., Zod, Joi), LIVR schemas are not JavaScript code and therefore cannot be used for code injection attacks.

    // Schema is pure data: can be stored in DB, sent over API, or loaded from config
    const schema = {
        username: ['required', { minLength: 3 }, { maxLength: 50 }],
        email: ['required', 'email'],
        role: { oneOf: ['admin', 'user', 'guest'] },
    };
    
    // Example: Loading and validating from a stored JSON string
    const loadedSchema = JSON.parse(await db.getSchema('user-registration'));
    const validator = new LIVR.Validator(loadedSchema);
  5. Cross-language validation with LIVR

    master

    Because LIVR schemas are defined as pure JSON data rather than executable code, they are language-agnostic. You can define a single schema and use it across different services written in different languages.

    Supported implementations include:

    • JavaScript/TypeScript: livr (npm)
    • Python: livr (PyPI)
    • PHP: validator-livr (Packagist)
    • Ruby: livr (RubyGems)
    • Java: livr-validator
    • Rust: livr (crates.io)
    • C++: cpp-validator-livr
    • Erlang: livr
    • Lua: livr
    • Perl: Validator::LIVR
  6. LIVR Design Philosophy: Zero Dependencies

    master

    LIVR is designed with a priority on supply chain security and lightweight deployment. To achieve this, the library maintains zero runtime dependencies. Every validation rule is implemented natively within the library rather than reusing existing third-party libraries.

    This approach provides several benefits for developers:

    • Security: Reduces the attack surface by eliminating transitive dependencies and potential vulnerabilities from upstream packages.
    • Auditability: Simplifies security compliance (e.g., SOC2, HIPAA) because there are no external dependency trees to review.
    • Stability: Avoids breaking changes from upstream packages and dependency version conflicts.
    • Performance: The minimal bundle size (approx. 4.5 KB gzipped) makes it ideal for Serverless/Edge computing (minimizing cold starts), frontend applications (improving page load times), and embedded/IoT systems.
  7. Understand LIVR's security and immutability defaults

    master

    LIVR is designed with a "secure by default" philosophy. Key behaviors include:

    1. Input Immutability: The validate() method never mutates the input object. It always returns a new object containing the validated/transformed data. This allows you to safely reuse the original input for logging or other purposes.
    2. Unknown Fields Stripped: By default, any fields present in the input that are not defined in the schema are automatically stripped from the output. This provides protection against mass assignment attacks and prototype pollution.
    3. Missing Optional Fields: Fields that are optional and missing from the input are omitted from the output object, preventing unexpected null or undefined values in your application logic.
  8. Inference for complex schemas (Nested, Lists, Unions)

    master

    LIVR supports deep type inference for complex structures:

    • Nested Objects: Recursively infers types for nested object definitions.
    • Lists: Uses list_of for arrays of primitives or list_of_objects for arrays of objects.
    • Discriminated Unions: Uses a discriminator field (e.g., type) within a list_of_different_objects rule to create polymorphic types.
    • Union Types: Uses the or rule to allow multiple types for a single field.
    // Discriminated Union Example
    const schema = {
        events: {
            list_of_different_objects: [
                'type', 
                {
                    click: {
                        type: { eq: 'click' as const },
                        x: ['required', 'integer'],
                        y: ['required', 'integer'],
                    },
                    scroll: {
                        type: { eq: 'scroll' as const },
                        direction: { one_of: ['up', 'down'] as const },
                    },
                },
            ],
        },
    } as const;
    
    type Data = InferFromSchema<typeof schema>;
    // { events?: Array<{ type: 'click'; x: number; y: number } | { type: 'scroll'; direction?: 'up' | 'down' }> }
  9. How LIVR handles required and optional fields

    master

    LIVR follows these rules for field presence in inferred types:

    1. Optional by default: Any field without a specific requirement rule is inferred as optional (field?: type).
    2. Required: Use the required rule to make a field mandatory (field: type).
    3. Default values: Using a default rule makes a field non-optional because it is guaranteed to have a value (field: type).
    const schema = {
        requiredField: ['required', 'string'],  // string (required)
        optionalField: 'string',                 // string | undefined (optional)
        count: [{ default: 0 }, 'integer'],     // number (has default)
        enabled: { default: true },              // boolean (has default)
    } as const;
    
    type Data = InferFromSchema<typeof schema>;
    // { requiredField: string; optionalField?: string; count: number; enabled: boolean }
  10. Handle type widening with `default`

    master

    The default rule automatically widens literal types to their primitive base (e.g., { default: 0 } becomes number, not 0).

    If you need to preserve a specific union of allowed values while using a default, use a type assertion on the default value.

    const schema = {
        count: { default: 0 },           // number
        name: { default: 'anonymous' },  // string
        active: { default: true },       // boolean
    
        // Use type assertions to preserve specific types
        status: { default: 'ACTIVE' as 'ACTIVE' | 'PENDING' }, 
    } as const;
    
    type Config = InferFromSchema<typeof schema>;
    // { count: number; name: string; active: boolean; status: 'ACTIVE' | 'PENDING'; }
  11. Advanced: Parameterized Rule Type Inference

    master

    For rules where the output type depends on the input arguments (e.g., one_of returning a union of its arguments), use ParameterizedRuleRegistry and ParameterizedRuleDef.

    Built-in Templates

    • literal: Output equals the argument type (e.g., eq).
    • array_element: Output is the element type of an array argument (e.g., one_of).
    • infer_schema: Output is inferred from a schema argument (e.g., nested_object).
    • infer_schema_array: Output is an array of inferred schema types (e.g., list_of_objects).
    • infer_rule_array: Output is an array of inferred rule types (e.g., list_of).

    Custom Templates

    Extend TemplateOutputRegistry to define custom logic for transforming arguments into output types.

    // my-rules/allowed_status.d.ts
    import type { ParameterizedRuleDef } from 'livr/types/inference';
    
    declare module 'livr/types/inference' {
        interface ParameterizedRuleRegistry {
            // Uses 'array_element' template: output is union of array elements
            allowed_status: ParameterizedRuleDef<'array_element', false, false>;
        }
    }
    
    // Usage
    const schema = {
        status: { allowed_status: ['pending', 'active', 'closed'] as const },
    } as const;
    
    type Data = InferFromSchema<typeof schema>;
    // { status?: 'pending' | 'active' | 'closed' }
  12. Optimize performance by reusing validators

    master

    LIVR is designed for high performance. To get the best results:

    1. Reuse validators: Construct the validator instance once and call .validate() many times. Creating a new validator inside a loop is significantly slower.
    2. Lazy compilation: Rules are compiled on the first validation. You can call .prepare() manually to perform this compilation during application warmup to avoid latency on the first request.
    // GOOD: Create once, use many times
    const validator = new LIVR.Validator(schema);
    for (const item of items) {
        const result = validator.validate(item);
    }
    
    // AVOID: Creating validator for each validation
    for (const item of items) {
        const validator = new LIVR.Validator(schema);
        const result = validator.validate(item);
    }