This small library provides a simple schema validation system for JavaScript/TypeScript. The library has basic types with opportunities for extending.
npm install @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:
- TypeScript First: Built with TypeScript in mind, it provides strong type inference and ensures type safety throughout your codebase.
- Extensibility: Easily extend the library with custom logic using the
extend
function. - Rich Features: Includes advanced features like preprocessing, transformations, piping, and refinements, which are not always available in similar libraries.
- Lightweight: None dependencies and a small footprint make it ideal for projects where performance and simplicity are key.
- Customizable: Offers fine-grained control over validation and error handling.
- 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.
- 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.
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 |
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 |
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 |
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.
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: [],
// }
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.`,
});
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.`,
});
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.`,
});
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.`,
});
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.`,
},
);
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.`,
});
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.`,
});
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.`,
},
);
Creates a schema that accepts any value.
const anySchema = s.any();
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());
Parses the given value according to the schema.
const result = stringSchema.parse('hello');
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.
Makes the schema optional.
const optionalSchema = stringSchema.optional();
Makes the schema nullable.
const nullableSchema = stringSchema.nullable();
Makes the schema nullish (nullable and optional).
const nullishSchema = stringSchema.nullish();
Sets a default value for the schema.
const defaultSchema = stringSchema.default('default value');
Transforms the parsed value using the provided callback.
const transformedSchema = s.string().transform((value) => value.toUpperCase());
Pipes the output of one schema into another schema for further validation or transformation.
const pipedSchema = s.string().pipe(s.number());
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",
});
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;
});
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',
// },
// },
// }
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' },
// ]
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'
Use transform
to modify the parsed value.
const transformSchema = s.string().transform((value) => value.toUpperCase());
const result = transformSchema.parse('hello');
console.log(result);
// 'HELLO'
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
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'
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 }
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".
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'
MIT