Show HN: FormData validation for TypeScript (Zod's most requested feature)

4 hours ago 1

Zero-dependency TypeScript validation with features Zod doesn't have. FormData validation, partial validation, JSON Schema export, and more. Perfect for Remix/Next.js.

npm version  MIT Tests TypeScript

We implemented the TOP REQUESTED features from Zod's issue tracker:

New in v2.1 (Just Added!)

  • FormData validation (60+ votes) - Zod doesn't have it!
  • Partial validation - Return valid data even with errors
  • JSON Schema export - Better than zod-to-json-schema
  • Schema metadata extraction - Auto-generate forms
  • XOR (exclusive OR) - Cleaner than Zod's approach
  • File validation - Size & MIME type checking
  • Schema descriptions - Rich metadata support

From v2.0 (Already Have It!)

That's 11 features Zod doesn't have!

Feature Zod lite-schema-check v2.1
Basic validation
Nested objects
Optional/nullable ⚠️ (confusing) Clearer
FormData validation We have it 🔥
Partial validation We have it 🔥
JSON Schema export ⚠️ (external) Built-in 🔥
Schema metadata ⚠️ (hard) Easy 🔥
XOR support ⚠️ (clunky) Clean 🔥
Circular validation We have it
Mixed async/sync We have it
Reversible transforms We have it
Bundle size ~2KB ~3KB minified
Battle-tested ✅ Years ⚠️ Growing
npm install lite-schema-check

🔥 FormData Validation (NEW!)

import { validateFormData, object, string, number, file } from 'lite-schema-check/v2'; const schema = object({ name: string.min(3), age: number.positive(), avatar: file({ maxSize: 5_000_000, mimeTypes: ['image/png', 'image/jpeg'] }) }); // In Remix/Next.js server action: export async function action({ request }) { const formData = await request.formData(); const result = validateFormData(formData, schema); if (result.success) { // Automatic type conversion! age is number, not string await createUser(result.data); } }

🔄 Partial Validation (NEW!)

import { validatePartial, object, string, number } from 'lite-schema-check/v2'; const schema = object({ name: string.min(3), email: string.email(), age: number.positive() }); const data = { name: 'John', // ✅ Valid email: 'not-email', // ❌ Invalid age: 30 // ✅ Valid }; const result = validatePartial(data, schema); // Save what's valid, show errors for what's not await saveDraft(result.validData); // { name: 'John', age: 30 } showErrors(result.invalidFields); // { email: {...} }

📄 JSON Schema Export (NEW!)

import { toJSONSchema, describe, optional } from 'lite-schema-check/v2'; const schema = object({ name: describe(string.min(3), 'User full name'), age: optional(number.positive()) }); const jsonSchema = toJSONSchema(schema); // { // type: 'object', // properties: {...}, // required: ['name'] // ✅ 'age' correctly NOT required! // }
import { validate, object, optional, array, union, literal } from 'lite-schema-check/v2'; // Define a schema with all the features const userSchema = object({ name: 'string', age: 'number', email: optional('string'), // Can be missing tags: array('string', { min: 1, max: 10 }), role: union( literal('admin'), literal('user'), literal('guest') ), profile: object({ bio: 'string', website: optional('string') }) }); // Validate data const result = validate(userData, userSchema); if (result.success) { console.log('Valid!', result.data); } else { console.log('Errors:', result.errors); // Detailed error paths: ['profile', 'bio'] }
import { validate } from 'lite-schema-check'; const schema = { name: 'string', age: 'number' }; const result = validate({ name: 'Alice', age: 30 }, schema); // { isValid: true, errors: [] }

1. 🔥 FormData Validation (60+ Votes!)

Problem: Zod has NO FormData support. Every Remix/Next.js dev needs this.

Our Solution:

import { validateFormData, object, string, number, file } from 'lite-schema-check/v2'; const schema = object({ name: string.min(3), age: number.positive(), avatar: file({ maxSize: 5_000_000, mimeTypes: ['image/png'] }) }); // Automatic type conversion: strings → numbers, File validation const result = validateFormData(formData, schema);

Status: ✅ We have it | ❌ Zod doesn't


Problem: Zod returns nothing if ANY field fails. Can't save partial data.

Our Solution:

import { validatePartial } from 'lite-schema-check/v2'; const result = validatePartial(data, schema); // Returns: // { // validData: { name: 'John', age: 30 }, // Save these! // invalidFields: { email: {...} } // Show errors // } await saveDraft(result.validData); // Save what works

Use Cases: Auto-save forms, progressive validation, data migration

Status: ✅ We have it | ❌ Zod doesn't


3. 🔥 JSON Schema Export (Better than zod-to-json-schema)

Problem: Zod needs external package with bugs (optional fields marked required)

Our Solution:

import { toJSONSchema, toOpenAPISchema, describe } from 'lite-schema-check/v2'; const schema = object({ name: describe(string.min(3), 'User full name'), age: optional(number.positive()) }); const jsonSchema = toJSONSchema(schema); // ✅ Optional fields correctly NOT in required array // ✅ Descriptions preserved // ✅ OpenAPI 3.0 compatible

Status: ✅ Built-in | ⚠️ Zod needs external package


4. ✅ Circular/Recursive Validation (Zod Issue #5346)

Problem: Zod users struggle with tree structures, categories, comment threads.

Our Solution:

import { object, array, lazy } from 'lite-schema-check/v2'; const categorySchema = object({ name: 'string', children: array(lazy(() => categorySchema)) }); // Validates infinitely nested categories!

Status: ✅ We have it | ❌ Zod doesn't (issue open since Oct 16, 2025)


2. ✅ Clearer Optional/Nullable API (Zod Issue #5348)

Problem: Zod's .required() on .optional() confuses users.

Our Solution:

import { object, optional, nullable, nullish } from 'lite-schema-check/v2'; const schema = object({ email: optional('string'), // Can be missing entirely phone: nullable('string'), // Can be null (must be present) bio: nullish('string') // Can be null OR undefined });

Status: ✅ Clearer than Zod's API


3. ✅ Mixed Sync/Async Validation (Zod Issue #5379)

Problem: Zod is all-sync or all-async, no mixing.

Our Solution:

import { object, async } from 'lite-schema-check/v2'; import { string } from 'lite-schema-check/presets'; const schema = object({ username: async('string', async (val) => { // Check database const exists = await checkUsernameExists(val); if (exists) throw new Error('Username taken'); }), email: string.email() // Sync validation }); // Validates efficiently

Status: ✅ We support selective async | ❌ Zod doesn't


4. ✅ Reversible Transforms (Codecs) (Zod Issues #5374, #5377)

Problem: Zod transforms are one-way. Users want to serialize back.

Our Solution:

import { codec } from 'lite-schema-check/v2'; const dateCodec = codec( 'string', (str) => new Date(str), // Parse (decode) (date) => date.toISOString() // Serialize (encode) ); // Can parse API responses AND serialize back!

Status: ✅ Bidirectional | ❌ Zod is one-way only


import { object, array } from 'lite-schema-check/v2'; const schema = object({ tags: array('string', { min: 1, max: 5 }) }); validate({ tags: ['js', 'ts'] }); // ✅ Valid validate({ tags: [] }); // ❌ Too few validate({ tags: ['a','b','c','d','e','f'] }); // ❌ Too many
const orderSchema = object({ customer: object({ name: 'string', address: object({ street: 'string', city: 'string', zip: 'number' }) }), items: array(object({ product: 'string', quantity: 'number' })) }); // Validates deeply nested structures

Unions & Literals (Enums)

const taskSchema = object({ status: union( literal('todo'), literal('in_progress'), literal('done') ), priority: union(literal(1), literal(2), literal(3)) }); // Type-safe enum-like validation
import { string } from 'lite-schema-check/presets'; const schema = object({ email: string.email(), url: string.url(), uuid: string.uuid(), password: string.min(8), username: string.regex(/^[a-z0-9_]+$/) });
import { number } from 'lite-schema-check/presets'; const schema = object({ age: number.positive(), rating: number.int(), price: number.min(0), discount: number.max(100) });

validate(input: unknown, schema: Schema): ValidationResult

Validates data against a schema.

assertValid(input: unknown, schema: Schema): asserts input

Throws if validation fails.

createValidator(schema: Schema): (input: unknown) => ValidationResult

Creates a reusable validator.

  • object(shape) - Define object schema
  • optional(schema) - Mark field as optional
  • nullable(schema) - Mark field as nullable
  • nullish(schema) - Mark field as nullable OR optional
  • array(items, constraints?) - Define array with optional min/max
  • union(...options) - Multiple type options
  • literal(value) - Exact value match
  • lazy(factory) - Circular/recursive schemas
  • async(schema, validator) - Async validation
  • codec(schema, decode, encode) - Reversible transforms
  • refine(schema, refiner) - Custom validation
  • transform(schema, transformer) - Data transformation

Full TypeScript support with type inference:

import { validate, object, ValidationResult } from 'lite-schema-check/v2'; const schema = object({ name: 'string', age: 'number' }); const result: ValidationResult = validate(data, schema); if (result.success) { // result.data is typed! }

When to Use lite-schema-check

  • 🔥 You're using Remix / Next.js - FormData validation built-in
  • 🔥 You need auto-save forms - Partial validation
  • 🔥 You need API docs - JSON Schema / OpenAPI export
  • 🔥 You're building forms - Schema metadata extraction
  • You need circular/recursive validation
  • You want clearer optional/nullable API
  • You need reversible transforms (codecs)
  • You need mixed sync/async validation
  • Bundle size matters (edge functions)
  • You prefer simpler, more predictable API
  • You need battle-tested production stability
  • You need the richest ecosystem
  • You need advanced TypeScript type inference
  • You're already using it (migration cost)
  • You don't need the features we have

Most Zod schemas translate directly:

// Zod z.object({ name: z.string(), age: z.number(), email: z.string().optional() }) // lite-schema-check object({ name: 'string', age: 'number', email: optional('string') })

Most schemas migrate directly with minimal changes.

Check the /examples folder:

  • 🔥 formdata-validation.ts - NEW! 5 FormData examples (Remix/Next.js)
  • 🔥 partial-validation.ts - NEW! 5 partial validation examples (auto-save)
  • 🔥 json-schema-metadata.ts - NEW! 6 JSON Schema examples (OpenAPI)
  • v2-vs-zod.ts - Feature comparison
  • zod-pain-points.ts - Solutions to Zod issues
  • Basic validation
  • Optional/nullable fields
  • Nested objects
  • Arrays with constraints
  • Unions & literals
  • String/number refinements
  • Circular validation (lazy)
  • Async validation
  • Codecs (reversible transforms)
  • 🔥 FormData validation
  • 🔥 Partial validation
  • 🔥 JSON Schema export
  • 🔥 Schema metadata
  • 🔥 XOR support
  • 🔥 File validation
  • 🔥 Schema descriptions
  • Schema composition (extend, merge, pick, omit)
  • Discriminated unions
  • Better error messages
  • Performance optimization
  • Framework integrations (Remix, Next.js plugins)

Contributions welcome! We're actively building features Zod users are asking for.

How to contribute:

  1. Watch Zod's issues
  2. Implement requested features
  3. Submit PR with tests
  4. Help us compete!

MIT

  • Zod - Inspiration and competition
  • ArkType - Performance ideas
  • io-ts - Type safety concepts

Made with ❤️ by developers frustrated with existing validation libraries.

Give us a try! We're actively solving problems Zod users are asking for.

Read Entire Article