Skip to content

mjancarik/esmj-schema

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Schema

This small library provides a simple schema validation system for JavaScript/TypeScript. The library has basic types with opportunities for extending.

Installation

npm install @esmj/schema

Why Use @esmj/schema?

@esmj/schema is a lightweight and flexible schema validation library designed for developers who need a simple yet powerful way to validate and transform data. Here are some reasons to choose this package:

  1. TypeScript First: Built with TypeScript in mind, it provides strong type inference and ensures type safety throughout your codebase.
  2. Extensibility: Easily extend the library with custom logic using the extend function.
  3. Rich Features: Includes advanced features like preprocessing, transformations, piping, and refinements, which are not always available in similar libraries.
  4. Lightweight: None dependencies and a small footprint make it ideal for projects where performance and simplicity are key.
  5. Customizable: Offers fine-grained control over validation and error handling.
  6. Performance: @esmj/schema is optimized for speed, making it one of the fastest schema validation libraries available. Whether you're creating schemas, parsing data, or handling errors, @esmj/schema consistently outperforms many popular alternatives. Its minimalistic design ensures low overhead, even in high-performance applications.

Performance Highlights

  • Schema Creation: Create schemas in as little as 0.02 ms, even for complex structures.
  • Parsing: Parse data with blazing-fast speeds, handling 1,000,000 iterations in under 300 ms.
  • Error Handling: Efficiently manage errors with minimal performance impact, processing 1,000,000 iterations in under 400 ms.

These performance metrics make @esmj/schema an excellent choice for both frontend and backend applications where speed and efficiency are critical.

Comparison with Similar Libraries

When choosing a schema validation library, bundle size can be an important factor, especially for frontend applications where minimizing JavaScript size is critical. Here's how @esmj/schema compares to other popular libraries:

Library Bundle Size (minified + gzipped)
@esmj/schema ~1,1 KB
Superstruct ~3.2 KB
Yup ~12.2 KB
Zod@3 ~13 KB
@zod/mini ~20,5 KB
Joi ~40,4 KB
Zod@4 ~40,8 KB
ArkType ~41,8 KB

Performance Comparison

Schema Creation Performance

Library 1 Schema 1,000 Schema 1,000,000 Schema
@esmj/schema 0.02 ms 4.93 ms 1.13 s
zod@3 0.08 ms 9.68 ms 8.53 s
@zod/mini 0.22 ms 39.77 ms 34.51 s
Yup 0.54 ms 14.03 ms 12.34 s
Superstruct 0.13 ms 3.67 ms 1.74 s
Joi 0.62 ms 31.60 ms 23.06 s
ArkType 0.37 ms 54.60 ms Infinity

Parsing Performance

Library 1 Iteration 1,000 Iterations 1,000,000 Iterations
@esmj/schema 0.05 ms 0.46 ms 267.93 ms
zod@3 0.14 ms 1.44 ms 897.89 ms
@zod/mini 0.23 ms 0.42 ms 199.08 ms
Yup 0.30 ms 9.49 ms 8.69 s
Superstruct 0.08 ms 4.18 ms 3.71 s
Joi 0.33 ms 3.35 ms 2.69 s
ArkType 0.08 ms 0.70 ms 576,80 ms

Error Handling Performance

Library 1 Iteration 1,000 Iterations 1,000,000 Iterations
@esmj/schema 0.03 ms 0.59 ms 365.32 ms
zod3 0.05 ms 2.09 ms 1.26 s
@zod/mini 0.07 ms 0.99 ms 545.12 ms
Yup 0.27 ms 19.28 ms 18.87 s
Superstruct 0.04 ms 8.62 ms 6.24 s
Joi 0.15 ms 4.13 ms 2.57 s
ArkType 0.07 ms 3.78 ms 2.87 s

Note: During the performance tests, @zod/mini was observed to consume 200% CPU, while other libraries used only 100% CPU. This may affect the interpretation of the results, especially in multi-threaded environments.

Usage

Basic Usage

import { s, type Infer} from '@esmj/schema';

const schema = s.object({
  username: s.string().optional().refine((val) => val.length <= 255, {
    message: "Username can't be more than 255 characters",
  }),
  password: s.string().default('unknown'),
  birthday: s.preprocess((value) => new Date(value), s.date()),
  account: s.string().default('0').transform((value) => Number.parseInt(value)).pipe(s.number()),
  money: s.number(),
  address: s.object({
    street: s.string(),
    city: s.string().optional(),
  }).default({ street: 'unknown' }),
  records: s.array(s.object({ name: s.string() })).default([]),
});

type schemaType = Infer<typeof schema>;

const result = schema.parse({
  username: 'john_doe',
  birthday: '2000-01-01T23:59:59.000Z',
  address: { city: 'New York' },
  money: 100,
});

console.log(result);
// {
//   username: 'john_doe',
//   password: 'unknown',
//   birthday: Date('2000-01-01T23:59:59.000Z'),
//   account: 0,
//   money: 100,
//   address: {
//     street: 'unknown',
//     city: 'New York',
//   },
//   records: [],
// }

Schema Types

s.string(options?)

Creates a string schema. You can optionally pass options to customize error messages.

const stringSchema = s.string({
  message: (value) => `Custom error: "${value}" is not a valid string.`,
});

s.number(options?)

Creates a number schema. You can optionally pass options to customize error messages.

const numberSchema = s.number({
  message: (value) => `Custom error: "${value}" is not a valid number.`,
});

s.boolean(options?)

Creates a boolean schema. You can optionally pass options to customize error messages.

const booleanSchema = s.boolean({
  message: (value) => `Custom error: "${value}" is not a valid boolean.`,
});

s.date(options?)

Creates a date schema. You can optionally pass options to customize error messages.

const dateSchema = s.date({
  message: (value) => `Custom error: "${value}" is not a valid date.`,
});

s.object(definition, options?)

Creates an object schema with the given definition. You can optionally pass options to customize error messages.

const objectSchema = s.object(
  {
    key: s.string(),
    value: s.number(),
  },
  {
    message: (value) => `Custom error: "${JSON.stringify(value)}" is not a valid object.`,
  },
);

s.array(definition, options?)

Creates an array schema with the given item definition. You can optionally pass options to customize error messages.

const arraySchema = s.array(s.string(), {
  message: (value) => `Custom error: "${JSON.stringify(value)}" is not a valid array.`,
});

s.enum(values, options?)

Creates an enum schema that validates against a predefined set of string values. You can optionally pass options to customize error messages.

  • values: An array of strings representing the allowed values for the enum. Each value must be a string.
const enumSchema = s.enum(['admin', 'user', 'guest'], {
  message: (value) => `Custom error: "${value}" is not a valid enum value.`,
});

s.union(definitions, options?)

Creates a schema that validates against multiple schemas (a union of schemas). The value must match at least one of the provided schemas. You can optionally pass options to customize error messages.

  • definitions: An array of schemas to validate against.
const schema = s.union([
  s.string(),
  s.number(),
  s.boolean(),
]);

const validString = schema.parse('hello');
console.log(validString);
// 'hello'

const validNumber = schema.parse(42);
console.log(validNumber);
// 42

const validBoolean = schema.parse(true);
console.log(validBoolean);
// true

const invalidValue = schema.safeParse({ key: 'value' });
console.log(invalidValue.success);
// false
console.log(invalidValue.error.message);
// Validation failed. Expected the value to match one of the schemas: "string" | "number" | "boolean", but received "object" with value "{"key":"value"}".

Use Case: The union method is useful when you need to validate data that can be of multiple types, such as a value that can be a string, number, or boolean.

const schema = s.union(
  [s.string(), s.number(), s.boolean()],
  {
    message: (value) => `Custom error: "${value}" does not match any of the union schemas.`,
  },
);

s.any()

Creates a schema that accepts any value.

const anySchema = s.any();

s.preprocess(callback, schema)

Creates a schema that preprocesses the input value using the provided callback before validating it with the given schema.

const preprocessSchema = s.preprocess((value) => new Date(value), s.date());

Schema Methods

parse(value)

Parses the given value according to the schema.

const result = stringSchema.parse('hello');

safeParse(value)

Safely parses the given value according to the schema, returning a success or error result.

const result = stringSchema.safeParse('hello'); 
// { success: true, data: 'hello' }

const errorResult = stringSchema.safeParse(123); 
// { success: false, error: { message: 'The value "123" must be type of string but is type of "number".' } }

Note: The error returned by safeParse is not a native Error instance. Instead, it is a plain object with the following structure:

type ErrorStructure = {
  message: string;
  cause?: {
    key?: string;
  };
};

This allows for easier serialization and debugging but may require additional handling if you expect a native Error instance.

optional()

Makes the schema optional.

const optionalSchema = stringSchema.optional();

nullable()

Makes the schema nullable.

const nullableSchema = stringSchema.nullable();

nullish()

Makes the schema nullish (nullable and optional).

const nullishSchema = stringSchema.nullish();

default(defaultValue)

Sets a default value for the schema.

const defaultSchema = stringSchema.default('default value');

transform(callback)

Transforms the parsed value using the provided callback.

const transformedSchema = s.string().transform((value) => value.toUpperCase());

pipe(schema)

Pipes the output of one schema into another schema for further validation or transformation.

const pipedSchema = s.string().pipe(s.number());

refine(validation, { message })

Adds a refinement to the schema with a custom validation function and error message.

const refinedSchema = s.string().refine((val) => val.length <= 255, {
  message: "String can't be more than 255 characters",
});

Extending Schemas

You can extend the schema system with custom logic.

import { extend, type StringSchemaInterface } from '@esmj/schema';

interface StringSchemaInterface {
  customMethod(value: string): string {}
}

extend((schema, validation, options) => {
  schema.customMethod = (value) => {
    // Custom logic

    return value;
  };

  return schema;
});

More Examples

Nested Objects

You can define schemas for deeply nested objects.

const nestedSchema = s.object({
  user: s.object({
    id: s.number(),
    profile: s.object({
      name: s.string(),
      age: s.number().optional(),
    }),
  }),
});

const result = nestedSchema.parse({
  user: {
    id: 1,
    profile: {
      name: 'John Doe',
    },
  },
});

console.log(result);
// {
//   user: {
//     id: 1,
//     profile: {
//       name: 'John Doe',
//     },
//   },
// }

Arrays with Validation

You can validate arrays with specific item schemas.

const arraySchema = s.array(s.object({ id: s.number(), name: s.string() }));

const result = arraySchema.parse([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
]);

console.log(result);
// [
//   { id: 1, name: 'Item 1' },
//   { id: 2, name: 'Item 2' },
// ]

Preprocessing Values

Use s.preprocess to transform input values before validation.

const preprocessSchema = s.preprocess(
  (value) => value.trim(),
  s.string().refine((val) => val.length > 0, { message: 'String cannot be empty' }),
);

const result = preprocessSchema.parse('   hello   ');

console.log(result);
// 'hello'

Transforming Values

Use transform to modify the parsed value.

const transformSchema = s.string().transform((value) => value.toUpperCase());

const result = transformSchema.parse('hello');

console.log(result);
// 'HELLO'

Piping Schemas

Pipe the output of one schema into another for further validation or transformation.

const pipedSchema = s.string()
  .transform((value) => Number.parseInt(value))
  .pipe(s.number().refine((val) => val > 0, { message: 'Number must be positive' }));

const result = pipedSchema.parse('42');

console.log(result);
// 42

Refining Values

Add custom validation logic with refine.

const refinedSchema = s.string().refine((val) => val.startsWith('A'), {
  message: 'String must start with "A"',
});

const result = refinedSchema.parse('Apple');

console.log(result);
// 'Apple'

Default Values

Set default values for optional fields.

const defaultSchema = s.object({
  name: s.string().default('Anonymous'),
  age: s.number().optional().default(18),
});

const result = defaultSchema.parse({});

console.log(result);
// { name: 'Anonymous', age: 18 }

Safe Parsing

Use safeParse to handle errors gracefully.

const safeSchema = s.number();

const result = safeSchema.safeParse('not a number');

if (!result.success) {
  console.error(result.error.message);
} else {
  console.log(result.data);
}
// Error: The value "not a number" must be type of number but is type of "string".

Combining Multiple Features

Combine multiple features like preprocessing, transformations, and refinements.

const combinedSchema = s.preprocess(
  (value) => value.trim(),
  s.string()
    .transform((value) => value.toUpperCase())
    .refine((val) => val.length <= 10, { message: 'String must be at most 10 characters' }),
);

const result = combinedSchema.parse('   hello   ');

console.log(result);
// 'HELLO'

License

MIT

About

Tiny library for simple schema runtime validation system for JavaScript/TypeScript.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published