npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@shadow-library/class-schema

v0.2.0

Published

Decorator-based JSON schema generator at runtime

Readme

@shadow-library/class-schema

npm version License: MIT

A powerful TypeScript decorator-based JSON schema generator that creates JSON schemas at runtime from class definitions. Perfect for API validation, documentation generation, and data transformation.

Features

  • 🎯 Decorator-based: Define schemas using simple decorators
  • 🚀 Runtime Generation: Generate JSON schemas at runtime
  • 🔧 TypeScript Support: Full TypeScript integration with type safety
  • 📦 Lightweight: Minimal dependencies (only deepmerge)
  • 🔄 Type Transformations: Built-in utility types (PartialType, PickType, OmitType)
  • 🏗️ Schema Composition: Support for inheritance and schema composition
  • 📋 Comprehensive Validation: Support for all JSON Schema validation keywords
  • 🎛️ Flexible Configuration: Customizable schema options and transformations
  • 🔗 Schema Registry: Manage and reuse schemas efficiently
  • Transform Factory: Built-in data transformation capabilities

Installation

# Using npm
npm install @shadow-library/class-schema

# Using yarn
yarn add @shadow-library/class-schema

# Using bun
bun add @shadow-library/class-schema

Peer Dependencies

# Using npm
npm install @shadow-library/common reflect-metadata

# Using yarn
yarn add @shadow-library/common reflect-metadata

# Using bun
bun add @shadow-library/common reflect-metadata

Quick Start

import { Schema, Field, ClassSchema, Integer } from '@shadow-library/class-schema';

@Schema()
class User {
  @Field({ format: 'email' })
  email: string;

  @Field(() => String, { minLength: 2, maxLength: 50 })
  name: string;

  @Field(() => Integer, { minimum: 0, maximum: 150 })
  age: number;

  @Field({ optional: true })
  bio?: string;
}

// Generate JSON Schema
const schema = ClassSchema.generate(User);
console.log(schema);

Output:

{
  "$id": "class-schema:User-1",
  "type": "object",
  "required": ["email", "name", "age"],
  "additionalProperties": false,
  "properties": {
    "email": { "type": "string", "format": "email" },
    "name": { "type": "string", "minLength": 2, "maxLength": 50 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "bio": { "type": "string" }
  }
}

Core Concepts

Schema Decorator

The @Schema() decorator marks a class as a schema definition:

@Schema({
  $id: 'MySchema', // Unique identifier
  title: 'My Schema', // Human-readable title
  description: 'A sample schema',
  minProperties: 1, // Minimum number of properties
  maxProperties: 10, // Maximum number of properties
  additionalProperties: false, // Disallow additional properties (default)
})
class MySchema {
  // ... fields
}

Note: By default, all generated object schemas set additionalProperties to false. Explicitly set additionalProperties: true or provide a class (e.g., String) to allow or type additional properties.

Field Decorator

The @Field() decorator defines schema properties with validation rules:

class Example {
  // Basic field
  @Field()
  name: string;

  // Field with validation
  @Field({ minLength: 3, maxLength: 50, pattern: '^[a-zA-Z]+$' })
  username: string;

  // Optional field
  @Field({ optional: true })
  description?: string;

  // Field with default value
  @Field({ format: 'date-time' })
  createdAt: string = new Date().toISOString();

  // Nullable field
  @Field({ nullable: true })
  lastLogin: string | null;

  // Conditional requirement
  @Field({ requiredIf: 'hasAddress' })
  streetAddress: string;

  @Field()
  hasAddress: boolean;
}

Field Types and Validation

String Fields

class StringExample {
  @Field({
    minLength: 5,
    maxLength: 100,
    pattern: '^[a-zA-Z0-9]+$',
    format: 'email', // email, date, date-time, uri, uuid, etc.
  })
  email: string;

  @Field({ enum: ['active', 'inactive', 'pending'] })
  status: string;
}

Number Fields

import { Integer } from '@shadow-library/class-schema';

class NumberExample {
  @Field(() => Number, {
    minimum: 0,
    maximum: 100,
    multipleOf: 0.5,
  })
  score: number;

  @Field(() => Integer, {
    minimum: 18,
    maximum: 120,
    exclusiveMinimum: 17,
  })
  age: number;
}

Boolean Fields

class BooleanExample {
  @Field()
  isActive: boolean;

  @Field({ const: true })
  termsAccepted: boolean;
}

Array Fields

@Schema()
class Tag {
  @Field()
  name: string;
}

class ArrayExample {
  @Field(() => [String], {
    minItems: 1,
    maxItems: 10,
    uniqueItems: true,
  })
  tags: string[];

  @Field(() => [Tag])
  categories: Tag[];

  @Field(() => [Number], {
    minItems: 3,
    maxItems: 3,
  })
  coordinates: number[]; // [x, y, z]
}

Object References

@Schema()
class Address {
  @Field()
  street: string;

  @Field()
  city: string;

  @Field({ pattern: '^[0-9]{5}$' })
  zipCode: string;
}

@Schema()
class User {
  @Field()
  name: string;

  @Field(() => Address)
  address: Address;

  @Field(() => [Address])
  previousAddresses: Address[];
}

Advanced Features

Schema Inheritance

@Schema()
class BaseEntity {
  @Field(() => String, { format: 'uuid' })
  id: string;

  @Field(() => String, { format: 'date-time' })
  createdAt: string;

  @Field(() => String, { format: 'date-time' })
  updatedAt: string;
}

@Schema({ title: 'User Schema' })
class User extends BaseEntity {
  @Field({ format: 'email' })
  email: string;

  @Field()
  name: string;
}

Pattern Properties

@Schema({
  patternProperties: {
    '^config_[a-zA-Z]+$': String,
  },
})
class DynamicConfig {
  @Field()
  version: string;

  // Allows properties like config_database, config_cache, etc.
  [key: string]: string;
}

Additional Properties

@Schema({ additionalProperties: String })
class FlexibleSchema {
  @Field()
  name: string;

  // Allows any additional string properties
  [key: string]: string;
}

// Or disable additional properties
@Schema({ additionalProperties: false })
class StrictSchema {
  @Field()
  name: string;
  // No additional properties allowed
}

Default behavior: If additionalProperties is not specified, it defaults to false on all object schemas generated by this library.

Type Helpers

Create derivative schemas using utility types:

import { PartialType, PickType, OmitType } from '@shadow-library/class-schema';

@Schema()
class User {
  @Field()
  id: string;

  @Field()
  email: string;

  @Field()
  name: string;

  @Field()
  password: string;
}

// All fields optional
class UpdateUserDto extends PartialType(User) {}

// Pick specific fields
class PublicUser extends PickType(User, ['id', 'email', 'name'] as const) {}

// Omit sensitive fields
class CreateUserDto extends OmitType(User, ['id'] as const) {}

Schema Composition

The SchemaComposer provides utilities for composing multiple schemas using JSON Schema's anyOf, oneOf, and discriminator patterns.

Using anyOf

Use anyOf when the data can match any of the specified schemas:

import { Schema, Field, SchemaComposer, ClassSchema } from '@shadow-library/class-schema';

@Schema({ $id: 'EmailContact' })
class EmailContact {
  @Field({ const: 'email' })
  type: 'email';

  @Field({ format: 'email' })
  address: string;
}

@Schema({ $id: 'PhoneContact' })
class PhoneContact {
  @Field({ const: 'phone' })
  type: 'phone';

  @Field()
  number: string;
}

// Create a union type using anyOf
const Contact = SchemaComposer.anyOf(EmailContact, PhoneContact);
const schema = ClassSchema.generate(Contact);

Using oneOf

Use oneOf when the data must match exactly one of the specified schemas:

const User = SchemaComposer.oneOf(NativeUser, OAuthUser);
const schema = ClassSchema.generate(User);

Using Discriminators

Use discriminator for efficient variant detection based on a discriminator property. This generates a JSON Schema with a discriminator object containing propertyName and mapping:

@Schema({ $id: 'NativeUser' })
class NativeUser {
  @Field({ const: 'native' })
  type: 'native';

  @Field()
  username: string;

  @Field()
  password: string;
}

@Schema({ $id: 'OAuthUser' })
class OAuthUser {
  @Field({ const: 'oauth' })
  type: 'oauth';

  @Field()
  username: string;

  @Field()
  provider: string;
}

// Create discriminated union with 'type' as the discriminator key
const User = SchemaComposer.discriminator('type', NativeUser, OAuthUser);
const schema = ClassSchema.generate(User);

console.log(schema);
// Output includes:
// {
//   "oneOf": [{ "$ref": "NativeUser" }, { "$ref": "OAuthUser" }],
//   "discriminator": {
//     "propertyName": "type",
//     "mapping": { "native": "NativeUser", "oauth": "OAuthUser" }
//   },
//   ...
// }

Nested Schema Composition

Schema composition works seamlessly with nested fields:

@Schema({ $id: 'Account' })
class Account {
  @Field()
  id: string;

  @Field(() => SchemaComposer.oneOf(NativeUser, OAuthUser))
  admin: NativeUser | OAuthUser;

  @Field(() => [SchemaComposer.discriminator('type', NativeUser, OAuthUser)])
  users: (NativeUser | OAuthUser)[];
}

Schema Registry

Manage multiple schemas efficiently:

import { SchemaRegistry } from '@shadow-library/class-schema';

const registry = new SchemaRegistry();

// Add schemas to registry
registry.addSchema(User);
registry.addSchema(Address);

// Get schema instance
const userSchema = registry.getSchema(User);
const jsonSchema = userSchema.getJSONSchema();

Data Transformation

The TransformerFactory provides powerful data transformation capabilities with two main methods: compile and maybeCompile.

Basic Transformation with compile

The compile method always returns a transformer function. If no fields match the filter, it returns a no-op function that passes data through unchanged:

import { TransformerFactory } from '@shadow-library/class-schema';

@Schema()
class Product {
  @Field()
  name: string;

  @Field(() => Number, { minimum: 0 })
  price: number;

  @Field(() => String, { format: 'date-time' })
  createdAt: string;
}

const schema = ClassSchema.generate(Product);

// Create transformer for date fields
const factory = new TransformerFactory(fieldSchema => fieldSchema.format === 'date-time');
const transformer = factory.compile(schema);
const data = {
  name: 'Laptop',
  price: 999.99,
  createdAt: '2023-01-01T12:00:00Z',
};

// Transform date strings to Date objects
const transformed = transformer(data, value => new Date(value));
console.log(transformed.createdAt instanceof Date); // true

Note on field handling: The transformer uses 'field' in data to check fields, so fields explicitly set to undefined or null are transformed, while missing keys are skipped. If the action returns undefined, the field is removed from the object. Array items that transform to undefined are filtered out.

// Transform to undefined - field is removed
transformer({ name: 'test' }, () => undefined); // {}

// Field with null - transformed (key exists)
transformer({ name: null }, () => 'xxx'); // { name: 'xxx' }

// Missing key - not transformed (action not called)
transformer({}, () => 'xxx'); // {}

Conditional Transformation with maybeCompile

The maybeCompile method returns null if no fields match the filter, allowing you to optimize performance by skipping transformation entirely:

@Schema()
class User {
  @Field()
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;
}

const schema = ClassSchema.generate(User);

// Try to create transformer for date fields (none exist in this schema)
const factory = new TransformerFactory(fieldSchema => fieldSchema.format === 'date-time');
const transformer = factory.maybeCompile(schema);

if (transformer) {
  // Only transform if there are matching fields
  const transformed = transformer(userData, value => new Date(value));
} else {
  // No transformation needed - use original data
  console.log('No date fields found, skipping transformation');
}

Performance Optimization Example

Use maybeCompile to avoid unnecessary transformations:

class DataProcessor {
  private dateTransformer: ((data: any, action: any) => any) | null;
  private sensitiveTransformer: ((data: any, action: any) => any) | null;

  constructor(schema: ParsedSchema) {
    // Only create transformers if needed
    const dateFactory = new TransformerFactory(field => field.format === 'date-time');
    this.dateTransformer = dateFactory.maybeCompile(schema);

    const sensitiveFactory = new TransformerFactory(field => (field as any).sensitive === true);
    this.sensitiveTransformer = sensitiveFactory.maybeCompile(schema);
  }

  processData(data: any) {
    let result = data;

    // Transform dates only if schema has date fields
    if (this.dateTransformer) {
      result = this.dateTransformer(result, value => new Date(value as string));
    }

    // Mask sensitive data only if schema has sensitive fields
    if (this.sensitiveTransformer) {
      result = this.sensitiveTransformer(result, () => '[REDACTED]');
    }

    return result;
  }
}

// Usage
const processor = new DataProcessor(userSchema);
const processedData = processor.processData(rawUserData);

Method Comparison

| Method | Return Type | Use Case | | -------------------------------- | --------------------- | ----------------------------------------------------------------- | | compile(schema) | Transformer | Always returns a function, use when you always want to transform | | maybeCompile(schema) | Transformer \| null | Returns null if no fields match, use for performance optimization | | hasTransformableFields(schema) | boolean | Check if schema has any transformable fields without compiling |

Checking for Transformable Fields

Use hasTransformableFields to check if a schema contains any fields that match the filter criteria, without actually compiling a transformer:

import { TransformerFactory, ClassSchema } from '@shadow-library/class-schema';

@Schema()
class User {
  @Field()
  id: string;

  @Field()
  name: string;

  @Field({ format: 'date-time' })
  createdAt: string;
}

@Schema()
class Product {
  @Field()
  id: string;

  @Field()
  name: string;

  @Field(() => Number)
  price: number;
}

const userSchema = ClassSchema.generate(User);
const productSchema = ClassSchema.generate(Product);

const dateFactory = new TransformerFactory(field => field.format === 'date-time');

// Check without compiling
console.log(dateFactory.hasTransformableFields(userSchema)); // true - has date-time field
console.log(dateFactory.hasTransformableFields(productSchema)); // false - no date-time fields

// Useful for conditional logic before expensive operations
if (dateFactory.hasTransformableFields(userSchema)) {
  const transformer = dateFactory.compile(userSchema);
  // Use transformer...
}

This method is useful when you need to:

  • Conditionally enable/disable features based on schema capabilities
  • Validate schemas before processing
  • Make routing decisions based on schema content
  • Avoid unnecessary transformer compilation in hot paths

Masking Sensitive Data for Logging

Use transformers to mask sensitive information when logging data:

import { TransformerFactory, FieldMetadata } from '@shadow-library/class-schema';

@Schema()
class User {
  @Field()
  id: string;

  @Field({ format: 'email' })
  email: string;

  @Field()
  name: string;

  @Field()
  @FieldMetadata({ sensitive: true })
  password: string;
}

const schema = ClassSchema.generate(User);

// Create transformer that detects sensitive fields
const maskingFactory = new TransformerFactory(fieldSchema => fieldSchema.sensitive === true);
const maskingTransformer = maskingFactory.compile(schema);

const userData = {
  id: 'user123',
  email: '[email protected]',
  name: 'John Doe',
  password: 'secretPassword123',
};

// Mask sensitive data for logging
const maskedData = maskingTransformer(userData, () => 'xxxx');

console.log('Original data:', userData);
console.log('Masked for logging:', maskedData);

// Output:
// Original data: {
//   id: 'user123',
//   email: '[email protected]',
//   name: 'John Doe',
//   password: 'secretPassword123'
// }
//
// Masked for logging: {
//   id: 'user123',
//   email: '[email protected]',
//   name: 'John Doe',
//   password: 'xxxx'
// }

Transforming Discriminated Unions

The TransformerFactory automatically detects and handles discriminated union schemas (anyOf/oneOf). It intelligently determines the correct variant based on the data and applies transformations only to matching fields.

Automatic Discriminator Detection

The factory automatically detects discriminators using three strategies (in order of priority):

  1. Const discriminator: Uses const values to identify variants
  2. Type discriminator: Uses JavaScript typeof to distinguish variants
  3. Enum discriminator: Uses non-overlapping enum values to distinguish variants

Important: Only required fields are considered when detecting discriminators. Optional fields are skipped because they may not be present in the data, making them unreliable for variant discrimination. Make sure your discriminator field is marked as required (not optional) in your schema.

@Schema({ $id: 'Cat' })
class Cat {
  @Field({ const: 'cat' })
  type: 'cat';

  @Field()
  @FieldMetadata({ sensitive: true })
  meow: string;
}

@Schema({ $id: 'Dog' })
class Dog {
  @Field({ const: 'dog' })
  type: 'dog';

  @Field()
  @FieldMetadata({ sensitive: true })
  bark: string;
}

// Create discriminated union
const Animal = SchemaComposer.oneOf(Cat, Dog);
const schema = ClassSchema.generate(Animal);

// Transformer automatically uses 'type' field as const discriminator
const factory = new TransformerFactory(field => field.sensitive === true);
const transformer = factory.compile(schema);

const catData = { type: 'cat', meow: 'meow meow' };
const dogData = { type: 'dog', bark: 'woof woof' };

console.log(transformer(catData, () => 'xxx')); // { type: 'cat', meow: 'xxx' }
console.log(transformer(dogData, () => 'xxx')); // { type: 'dog', bark: 'xxx' }

Type-Based Discriminator

When variants have fields with different JavaScript types:

@Schema({ $id: 'StringVariant' })
class StringVariant {
  @Field()
  value: string;

  @Field()
  @FieldMetadata({ tagged: true })
  strField: string;
}

@Schema({ $id: 'NumberVariant' })
class NumberVariant {
  @Field(() => Number)
  value: number;

  @Field(() => Number)
  @FieldMetadata({ tagged: true })
  numField: number;
}

const Mixed = SchemaComposer.oneOf(StringVariant, NumberVariant);
const schema = ClassSchema.generate(Mixed);

// Factory detects 'value' field has different types and uses typeof for discrimination
const factory = new TransformerFactory(field => field.tagged === true);
const transformer = factory.compile(schema);

transformer({ value: 'hello', strField: 'world' }, () => 'xxx'); // { value: 'hello', strField: 'xxx' }
transformer({ value: 42, numField: 100 }, () => 999); // { value: 42, numField: 999 }

Enum-Based Discriminator

When variants have non-overlapping enum values:

@Schema({ $id: 'SmallSize' })
class SmallSize {
  @Field({ enum: ['xs', 'sm'] })
  size: 'xs' | 'sm';

  @Field()
  @FieldMetadata({ tagged: true })
  smallField: string;
}

@Schema({ $id: 'LargeSize' })
class LargeSize {
  @Field({ enum: ['lg', 'xl'] })
  size: 'lg' | 'xl';

  @Field()
  @FieldMetadata({ tagged: true })
  largeField: string;
}

const Size = SchemaComposer.oneOf(SmallSize, LargeSize);
const schema = ClassSchema.generate(Size);

const factory = new TransformerFactory(field => field.tagged === true);
const transformer = factory.compile(schema);

transformer({ size: 'xs', smallField: 'tiny' }, () => 'xxx'); // { size: 'xs', smallField: 'xxx' }
transformer({ size: 'xl', largeField: 'huge' }, () => 'xxx'); // { size: 'xl', largeField: 'xxx' }

Fallback Behaviour

When no valid discriminator can be determined (e.g., overlapping enums or no distinguishing fields), the transformer applies all variant transformers to the data:

@Schema({ $id: 'VariantA' })
class VariantA {
  @Field()
  @FieldMetadata({ tagged: true })
  fieldA: string;
}

@Schema({ $id: 'VariantB' })
class VariantB {
  @Field()
  @FieldMetadata({ tagged: true })
  fieldB: string;
}

const Union = SchemaComposer.oneOf(VariantA, VariantB);
const schema = ClassSchema.generate(Union);

const factory = new TransformerFactory(field => field.tagged === true);
const transformer = factory.compile(schema);

// Both transformers are applied since no discriminator exists
transformer({ fieldA: 'a', fieldB: 'b' }, () => 'xxx'); // { fieldA: 'xxx', fieldB: 'xxx' }

Best Practice: Always ensure nested $ref objects are valid objects before transformation, or handle potential errors when the nested object might be null or undefined.

Array Schemas

Generate schemas for arrays:

@Schema()
class Item {
  @Field()
  name: string;

  @Field()
  value: number;
}

// Create array schema
const arraySchema = ClassSchema.generate([Item]);

console.log(arraySchema);
// Output:
// {
//   "$id": "Item?type=Array",
//   "type": "array",
//   "items": { "$ref": "Item" },
//   "definitions": {
//     "Item": { ... }
//   }
// }

Working with Primitive Types

Generate schemas for primitive types:

// String schema
const stringSchema = ClassSchema.generate(String);
// { "$id": "String", "type": "string" }

// Number schema
const numberSchema = ClassSchema.generate(Number);
// { "$id": "Number", "type": "number" }

// Boolean schema
const booleanSchema = ClassSchema.generate(Boolean);
// { "$id": "Boolean", "type": "boolean" }

// Array schema
const arraySchema = ClassSchema.generate(Array);
// { "$id": "Array", "type": "array" }

// Object schema
const objectSchema = ClassSchema.generate(Object);
// { "$id": "Object", "type": "object" }

Configuration Options

Schema Options

interface SchemaOptions {
  $id?: string; // Schema identifier
  title?: string; // Schema title
  description?: string; // Schema description
  maxProperties?: number; // Maximum properties
  minProperties?: number; // Minimum properties
  patternProperties?: Record<string, Class<unknown>>;
  additionalProperties?: boolean | Class<unknown>; // Defaults to false
  if?: JSONSchema; // Conditional schema: if
  then?: JSONSchema; // Conditional schema: then
  else?: JSONSchema; // Conditional schema: else
}

Use if/then/else to embed JSON Schema conditional logic directly in the decorator:

@Schema({
  if: { properties: { type: { const: 'a' } } },
  then: { required: ['aOnly'] },
  else: { required: ['bOnly'] },
})
class ConditionalExample {
  @Field({ const: 'a' })
  type: 'a' | 'b';

  @Field({ optional: true })
  aOnly?: string;

  @Field({ optional: true })
  bOnly?: string;
}

Field Options

interface BaseFieldSchema<T> {
  enum?: T[]; // Allowed values
  examples?: T[]; // Example values
  optional?: boolean; // Optional field
  requiredIf?: string; // Conditional requirement
  description?: string; // Field description
  const?: T; // Constant value
  nullable?: boolean; // Allow null values
}

Error Handling

try {
  // This will throw an error if class is not decorated with @Schema
  const schema = ClassSchema.generate(UndecoratedClass);
} catch (error) {
  console.error(error.message);
  // "Class 'UndecoratedClass' is not a schema. Add the @Schema() to the class"
}

Integration Examples

With Express.js

import express from 'express';
import Ajv from 'ajv';
import { ClassSchema } from '@shadow-library/class-schema';

const app = express();
const ajv = new Ajv();

@Schema({ $id: 'CreateUserRequest' })
class CreateUserRequest {
  @Field({ format: 'email' })
  email: string;

  @Field({ minLength: 2 })
  name: string;
}

app.post('/users', (req, res) => {
  const schema = ClassSchema.generate(CreateUserRequest);
  const validate = ajv.compile(schema);

  if (!validate(req.body)) {
    return res.status(400).json({ errors: validate.errors });
  }

  // Process valid data...
});

With Fastify

import Fastify from 'fastify';
import { ClassSchema } from '@shadow-library/class-schema';

const fastify = Fastify();

@Schema({ $id: 'User' })
class User {
  @Field()
  name: string;

  @Field({ format: 'email' })
  email: string;
}

fastify.route({
  method: 'POST',
  url: '/users',
  schema: {
    body: ClassSchema.generate(User),
  },
  handler: async (request, reply) => {
    // Request body is automatically validated
    return { success: true };
  },
});

TypeScript Integration

The library provides full TypeScript support:

// Type-safe field picking
const keys = ['id', 'name'] as const;
class UserSummary extends PickType(User, keys) {}

// Type inference works correctly
const summary: UserSummary = {
  id: '123',
  name: 'John',
  // email is not required or allowed
};

Best Practices

  1. Use descriptive schema IDs: Always provide meaningful $id values
  2. Validate early: Generate schemas at application startup
  3. Reuse schemas: Use SchemaRegistry for better performance
  4. Type safety: Leverage TypeScript's type system with utility types
  5. Document schemas: Use title and description options
  6. Version schemas: Include version information in schema IDs when needed

API Reference

Classes

  • ClassSchema<T>: Main schema generator class
  • SchemaRegistry: Registry for managing multiple schemas
  • SchemaComposer: Utility for composing multiple schemas with anyOf, oneOf, and discriminators
  • TransformerFactory: Factory for creating data transformers

SchemaComposer Static Methods

SchemaComposer.anyOf(...Classes)

Creates a schema that matches any of the provided classes:

const Contact = SchemaComposer.anyOf(EmailContact, PhoneContact);
SchemaComposer.oneOf(...Classes)

Creates a schema that matches exactly one of the provided classes:

const User = SchemaComposer.oneOf(NativeUser, OAuthUser);
SchemaComposer.discriminator(key, ...Classes)

Creates a discriminated union schema with automatic mapping generation:

const User = SchemaComposer.discriminator('type', NativeUser, OAuthUser);
// Generates schema with discriminator.mapping based on const values

ClassSchema Static Methods

ClassSchema.generate(Class)

Generates a JSON schema from a class definition:

@Schema()
class User {
  @Field()
  name: string;

  @Field()
  email: string;
}

const schema = ClassSchema.generate(User);
console.log(schema);
// Returns a branded ParsedSchema with complete JSON schema
ClassSchema.isBranded(schema)

Checks if a schema object was generated by this package. Returns true for schemas created by ClassSchema.generate() or new ClassSchema(), false for plain JSON schema objects:

@Schema()
class Product {
  @Field()
  name: string;
}

// Generated schema is branded
const generatedSchema = ClassSchema.generate(Product);
console.log(ClassSchema.isBranded(generatedSchema)); // true

// Clone also preserves the brand
const clonedSchema = new ClassSchema(Product).getJSONSchema(true);
console.log(ClassSchema.isBranded(clonedSchema)); // true

// Plain JSON schema objects are not branded
const plainSchema = { $id: 'Plain', type: 'object' };
console.log(ClassSchema.isBranded(plainSchema)); // false

Use Cases:

  • Validation: Ensure schemas are from this package before using with TransformerFactory
  • Type Safety: Verify schema authenticity in runtime checks
  • Error Prevention: Avoid passing incompatible schemas to package methods
// TransformerFactory uses isBranded internally for validation
const factory = new TransformerFactory(field => field.format === 'date');

try {
  const transformer = factory.maybeCompile(generatedSchema); // ✅ Works
} catch (error) {
  // Won't happen - schema is branded
}

try {
  const transformer = factory.maybeCompile(plainSchema); // ❌ Throws error
} catch (error) {
  console.log(error.message);
  // "Invalid schema: only schemas built with this package are supported"
}

TransformerFactory Methods

new TransformerFactory(filter)

Creates a new transformer factory with a field filter function:

const factory = new TransformerFactory(fieldSchema => fieldSchema.format === 'date-time');
factory.compile(schema)

Compiles a transformer function. Returns a no-op function if no fields match the filter:

const transformer = factory.compile(schema);
const result = transformer(data, (value, fieldSchema, ctx) => transformedValue);
factory.maybeCompile(schema)

Compiles a transformer function. Returns null if no fields match the filter:

const transformer = factory.maybeCompile(schema);
if (transformer) {
  const result = transformer(data, (value, fieldSchema, ctx) => transformedValue);
}
factory.hasTransformableFields(schema)

Checks if a schema contains any fields matching the filter criteria. Returns true if transformable fields exist, false otherwise. Throws an error if the schema is not branded:

const factory = new TransformerFactory(fieldSchema => fieldSchema.format === 'date-time');

if (factory.hasTransformableFields(schema)) {
  // Schema has date-time fields
  const transformer = factory.compile(schema);
}

Decorators

  • @Schema(options?): Mark class as schema
  • @Field(options?): Define field schema
  • @Field(typeFn, options?): Define field with explicit type

Utility Functions

  • PartialType<T>(Class): Create partial version of schema
  • PickType<T, K>(Class, keys): Pick specific fields
  • OmitType<T, K>(Class, keys): Omit specific fields

Constants

  • Integer: Type for integer fields

License

MIT © shadow-library

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Support


Built with ❤️ by the Shadow Library team