unischema 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +780 -228
  2. package/dist/adapters/backend/index.d.mts +2 -1
  3. package/dist/adapters/backend/index.d.ts +2 -1
  4. package/dist/adapters/backend/index.js +17 -441
  5. package/dist/adapters/backend/index.mjs +9 -433
  6. package/dist/adapters/frontend/index.d.mts +2 -1
  7. package/dist/adapters/frontend/index.d.ts +2 -1
  8. package/dist/adapters/frontend/index.js +10 -421
  9. package/dist/adapters/frontend/index.mjs +8 -419
  10. package/dist/chunk-5A4ITJVD.mjs +124 -0
  11. package/dist/chunk-66RFUBVU.js +131 -0
  12. package/dist/chunk-75YSYC4K.mjs +85 -0
  13. package/dist/chunk-76BBWQDH.js +90 -0
  14. package/dist/chunk-7XES4A3M.mjs +237 -0
  15. package/dist/chunk-BVRXGZLS.js +17 -0
  16. package/dist/chunk-COMVAVFU.mjs +335 -0
  17. package/dist/chunk-DT2TQZU7.js +796 -0
  18. package/dist/chunk-FPCCH55A.js +103 -0
  19. package/dist/chunk-IUXRLMET.js +206 -0
  20. package/dist/chunk-JEW6U6CB.js +353 -0
  21. package/dist/chunk-KZCV5IW4.mjs +97 -0
  22. package/dist/chunk-KZZ7NVU3.mjs +41 -0
  23. package/dist/chunk-MFEBMQAU.mjs +779 -0
  24. package/dist/chunk-OIYG5D2I.js +50 -0
  25. package/dist/chunk-RW6HDA5H.mjs +194 -0
  26. package/dist/chunk-TTK77YBI.mjs +15 -0
  27. package/dist/chunk-TXT36BCE.js +248 -0
  28. package/dist/index-C17xs-fU.d.mts +140 -0
  29. package/dist/index-C17xs-fU.d.ts +140 -0
  30. package/dist/index.d.mts +26 -7
  31. package/dist/index.d.ts +26 -7
  32. package/dist/index.js +769 -499
  33. package/dist/index.mjs +695 -487
  34. package/dist/{schema-D9DGC9E_.d.ts → schema-DYE8Wz8X.d.mts} +264 -79
  35. package/dist/{schema-D9DGC9E_.d.mts → schema-Dtp-joeT.d.ts} +264 -79
  36. package/dist/validators/array.d.mts +15 -0
  37. package/dist/validators/array.d.ts +15 -0
  38. package/dist/validators/array.js +31 -0
  39. package/dist/validators/array.mjs +2 -0
  40. package/dist/validators/common.d.mts +13 -0
  41. package/dist/validators/common.d.ts +13 -0
  42. package/dist/validators/common.js +27 -0
  43. package/dist/validators/common.mjs +2 -0
  44. package/dist/validators/date.d.mts +23 -0
  45. package/dist/validators/date.d.ts +23 -0
  46. package/dist/validators/date.js +47 -0
  47. package/dist/validators/date.mjs +2 -0
  48. package/dist/validators/index.d.mts +46 -0
  49. package/dist/validators/index.d.ts +46 -0
  50. package/dist/validators/index.js +256 -0
  51. package/dist/validators/index.mjs +7 -0
  52. package/dist/validators/number.d.mts +25 -0
  53. package/dist/validators/number.d.ts +25 -0
  54. package/dist/validators/number.js +51 -0
  55. package/dist/validators/number.mjs +2 -0
  56. package/dist/validators/object.d.mts +11 -0
  57. package/dist/validators/object.d.ts +11 -0
  58. package/dist/validators/object.js +23 -0
  59. package/dist/validators/object.mjs +2 -0
  60. package/dist/validators/string.d.mts +37 -0
  61. package/dist/validators/string.d.ts +37 -0
  62. package/dist/validators/string.js +75 -0
  63. package/dist/validators/string.mjs +2 -0
  64. package/package.json +82 -5
  65. package/dist/adapters/backend/index.js.map +0 -1
  66. package/dist/adapters/backend/index.mjs.map +0 -1
  67. package/dist/adapters/frontend/index.js.map +0 -1
  68. package/dist/adapters/frontend/index.mjs.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/index.mjs.map +0 -1
package/README.md CHANGED
@@ -1,368 +1,920 @@
1
- # FormSchema
1
+ # Unischema
2
2
 
3
- **Schema-Driven, Isomorphic Form & Validation Engine**
3
+ **The Universal Schema-Driven Validation Library**
4
4
 
5
- FormSchema is a TypeScript-first validation library that provides a single source of truth for form validation across your entire stack. Define your schema once, use it everywhere — frontend, backend, and type system.
5
+ [![npm version](https://badge.fury.io/js/unischema.svg)](https://www.npmjs.com/package/unischema)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
- ## The Problem
8
+ Unischema is a TypeScript-first validation library that provides **one schema, everywhere**. Define your validation once, use it on frontend, backend, and get automatic TypeScript types.
8
9
 
9
- In traditional applications:
10
- - Frontend has its own validation (vee-validate, Yup, Zod)
11
- - Backend has its own validation (Joi, AJV, class-validator)
12
- - They often drift apart, causing bugs
13
- - No type safety between the two
14
- - Duplicate code, duplicate bugs
10
+ ## 🚀 Why Unischema?
15
11
 
16
- ## The Solution
12
+ **The Problem:**
13
+ - Frontend has validation (Yup, Zod, vee-validate)
14
+ - Backend has validation (Joi, AJV, class-validator)
15
+ - They drift apart → bugs
16
+ - No type safety between them
17
+ - Code duplication everywhere
17
18
 
18
- FormSchema provides:
19
- - **Single executable schema** that runs unchanged in browser and Node.js
20
- - **Automatic TypeScript types** inferred from your schema
21
- - **Hard + Soft validation** (errors vs warnings) for enterprise patterns
22
- - **Framework-agnostic core** with adapters for Express, serverless, and any frontend
23
- - **Zero duplicated logic** — one schema, everywhere
19
+ **The Solution:**
20
+ ```typescript
21
+ // Define once
22
+ const UserSchema = schema({
23
+ email: field.string().email().required(),
24
+ age: field.number().min(18)
25
+ });
26
+
27
+ // ✅ Use on backend (Express)
28
+ app.post('/users', validateBody(UserSchema), handler);
29
+
30
+ // ✅ Use on frontend (any framework)
31
+ const form = createForm(UserSchema, { onSubmit });
24
32
 
25
- ## Installation
33
+ // ✅ Get TypeScript types automatically
34
+ type User = InferInput<typeof UserSchema>;
35
+ ```
36
+
37
+ ## 📦 Installation
26
38
 
27
39
  ```bash
28
- npm install formschema
40
+ npm install unischema
29
41
  ```
30
42
 
31
- ## Quick Start
43
+ ## Quick Start
32
44
 
33
- ### 1. Define Your Schema (Once)
45
+ ### 1️⃣ Define Your Schema
34
46
 
35
47
  ```typescript
36
- // schemas/user.ts
37
- import { schema, field, type InferInput } from 'formschema';
48
+ import { schema, field } from 'unischema';
38
49
 
39
50
  export const UserSchema = schema({
40
51
  email: field.string()
41
- .email('Invalid email address')
52
+ .email('Invalid email')
42
53
  .required('Email is required'),
43
54
 
44
55
  password: field.string()
45
- .min(8, 'Password must be at least 8 characters')
46
- .required('Password is required'),
56
+ .min(8, 'At least 8 characters')
57
+ .required(),
47
58
 
48
59
  age: field.number()
49
- .min(13, 'Must be at least 13') // Hard validation
50
- .minSoft(18, 'Parental consent required'), // Soft warning
60
+ .min(18, 'Must be 18+')
61
+ .max(120),
51
62
  });
52
-
53
- // TypeScript type is automatically inferred
54
- export type UserInput = InferInput<typeof UserSchema>;
55
- // { email: string; password: string; age: number }
56
63
  ```
57
64
 
58
- ### 2. Use on Backend (Express)
65
+ ### 2️⃣ Use on Backend
59
66
 
60
67
  ```typescript
61
- // server.ts
62
68
  import express from 'express';
63
- import { validateBody, type ValidatedRequest } from 'formschema/backend';
64
- import { UserSchema, type UserInput } from './schemas/user';
69
+ import { validateBody } from 'unischema/backend';
70
+ import { UserSchema } from './schemas';
65
71
 
66
72
  const app = express();
67
- app.use(express.json());
68
-
69
- app.post('/api/users',
70
- validateBody(UserSchema),
71
- (req: ValidatedRequest<UserInput>, res) => {
72
- // req.validatedData is typed and validated!
73
- const { email, password, age } = req.validatedData;
74
-
75
- // Check for warnings
76
- if (req.validationResult.softErrors.length > 0) {
77
- console.log('Warnings:', req.validationResult.softErrors);
78
- }
79
73
 
80
- res.json({ success: true, email });
81
- }
82
- );
74
+ app.post('/register', validateBody(UserSchema), (req, res) => {
75
+ const { email, password, age } = req.validatedData; // ✅ Typed & validated
76
+ res.json({ success: true });
77
+ });
83
78
  ```
84
79
 
85
- ### 3. Use on Frontend
80
+ ### 3️⃣ Use on Frontend
86
81
 
87
82
  ```typescript
88
- // form.ts
89
- import { createForm, focusFirstError } from 'formschema/frontend';
90
- import { UserSchema } from './schemas/user';
83
+ import { createForm } from 'unischema/frontend';
84
+ import { UserSchema } from './schemas';
91
85
 
92
86
  const form = createForm(UserSchema, {
93
87
  onSubmit: async (values) => {
94
- const response = await fetch('/api/users', {
88
+ await fetch('/register', {
95
89
  method: 'POST',
96
- body: JSON.stringify(values),
90
+ body: JSON.stringify(values)
97
91
  });
98
- // Handle response...
99
- },
92
+ }
100
93
  });
101
94
 
102
- // Get field props for your UI
95
+ // Get field props for your UI framework
103
96
  const emailProps = form.getFieldProps('email');
104
- // { name, value, onChange, onBlur, error, hasError, ... }
105
97
  ```
106
98
 
107
- ## Core Concepts
99
+ ## 🎯 Features
100
+
101
+ - ✅ **53+ Built-in Validators** - Email, URL, IPv4/IPv6, phone, coordinates, and more
102
+ - ✅ **Async Validation** - Database checks, API validation with debouncing
103
+ - ✅ **Data Transformation** - Transform & coerce values before validation
104
+ - ✅ **Advanced Schema Composition** - deepPartial, passthrough, strict, catchall
105
+ - ✅ **Isomorphic** - Same code runs in browser and Node.js
106
+ - ✅ **TypeScript First** - Automatic type inference
107
+ - ✅ **Hard & Soft Validation** - Errors vs warnings for enterprise apps
108
+ - ✅ **Nullable/Nullish Support** - Proper null/undefined handling
109
+ - ✅ **Tree-Shakeable** - Only bundle what you use (~2KB min+gzip)
110
+ - ✅ **Framework Agnostic** - Works with React, Vue, Svelte, Angular, etc.
111
+ - ✅ **Zero Dependencies** - Lightweight and fast
112
+
113
+ ## 🆕 What's New in v1.2.0 - Production Ready!
114
+
115
+ Phase 1 is complete! Unischema now includes powerful features for production applications:
108
116
 
109
- ### Hard vs Soft Validation
117
+ ### Async Validation
110
118
 
111
- FormSchema supports two-tier validation, essential for enterprise applications:
119
+ Validate against external APIs, databases, or async operations with built-in debouncing:
112
120
 
113
121
  ```typescript
114
- const TransactionSchema = schema({
115
- amount: field.number()
116
- .min(0.01, 'Amount must be positive') // Hard: blocks submission
117
- .maxSoft(10000, 'Large transaction - review'), // Soft: warning only
122
+ const UserSchema = schema({
123
+ email: field.string()
124
+ .email()
125
+ .refineAsync(async (email) => {
126
+ const exists = await checkEmailExists(email);
127
+ return !exists || { message: 'Email already registered' };
128
+ }, { debounce: 500, timeout: 5000 }),
129
+
130
+ username: field.string()
131
+ .refineAsync(async (name) => {
132
+ const available = await api.checkUsername(name);
133
+ return available;
134
+ }, { debounce: 300, message: 'Username taken' })
118
135
  });
119
136
 
120
- const result = validate(TransactionSchema.definition, { amount: 50000 });
121
-
122
- result.valid; // true - no hard errors
123
- result.hardErrors; // []
124
- result.softErrors; // [{ field: 'amount', message: 'Large transaction...', severity: 'soft' }]
137
+ // Use async validation
138
+ const result = await validateAsync(UserSchema.definition, data);
125
139
  ```
126
140
 
127
- **Hard validations** block form submission. **Soft validations** are warnings that don't block.
128
-
129
- ### Schema Composition
141
+ ### Data Transformation & Coercion
130
142
 
131
- Build complex schemas from reusable parts:
143
+ Transform and coerce values before validation:
132
144
 
133
145
  ```typescript
134
- const AddressSchema = schema({
135
- street: field.string().required(),
136
- city: field.string().required(),
137
- zipCode: field.string().pattern(/^\d{5}$/),
146
+ // Transform strings
147
+ const LoginSchema = schema({
148
+ email: field.string()
149
+ .transform(s => s.trim())
150
+ .transform(s => s.toLowerCase())
151
+ .email(),
152
+
153
+ name: field.string()
154
+ .transform(s => s.trim())
155
+ .transform(s => s.replace(/\s+/g, ' ')) // Normalize whitespace
138
156
  });
139
157
 
140
- const UserSchema = schema({
141
- name: field.string().required(),
142
- address: field.object(AddressSchema).required(),
158
+ // Type coercion from form inputs
159
+ const FormSchema = schema({
160
+ age: coerce.number().min(18), // "25" → 25
161
+ active: coerce.boolean(), // "true" → true
162
+ startDate: coerce.date(), // "2024-01-01" → Date
163
+ tags: coerce.array(field.string()), // "javascript" → ["javascript"]
143
164
  });
144
165
 
145
- // Extend schemas
146
- const AdminSchema = extend(UserSchema, {
147
- role: field.string().enum(['admin', 'superadmin']),
166
+ // Preprocessing for nullable values
167
+ const ProfileSchema = schema({
168
+ bio: field.string()
169
+ .preprocess(s => s?.trim()) // Handle null/undefined safely
170
+ .nullable()
148
171
  });
172
+ ```
173
+
174
+ ### Advanced Schema Composition
149
175
 
150
- // Pick/Omit fields
151
- const PublicUserSchema = omit(UserSchema, ['password']);
152
- const LoginSchema = pick(UserSchema, ['email', 'password']);
176
+ More flexible schema manipulation:
177
+
178
+ ```typescript
179
+ const BaseSchema = schema({
180
+ id: field.string(),
181
+ name: field.string().required(),
182
+ email: field.string().email().required(),
183
+ });
184
+
185
+ // Deep partial - make all fields optional recursively
186
+ const PartialSchema = deepPartial(BaseSchema);
187
+
188
+ // Passthrough - allow unknown keys
189
+ const FlexibleSchema = passthrough(BaseSchema);
190
+
191
+ // Strict mode - reject unknown keys
192
+ const StrictSchema = strict(BaseSchema);
193
+
194
+ // Catchall - handle unknown keys with validation
195
+ const CatchAllSchema = catchall(BaseSchema, field.string());
196
+
197
+ // Make specific fields required/optional
198
+ const RequiredFields = required(BaseSchema, ['name', 'email']);
199
+ const OptionalFields = optional(BaseSchema, ['email']);
153
200
  ```
154
201
 
155
- ### Type Inference
202
+ ### Nullable & Nullish Handling
156
203
 
157
- Types are automatically inferred from your schema:
204
+ Better null/undefined value handling:
158
205
 
159
206
  ```typescript
160
207
  const UserSchema = schema({
161
- email: field.string().email().required(),
162
- age: field.number().min(0),
163
- tags: field.array(field.string()),
208
+ // Allow null
209
+ middleName: field.string().nullable(), // string | null
210
+
211
+ // Allow null or undefined
212
+ bio: field.string().nullish(), // string | null | undefined
213
+
214
+ // Required but nullable
215
+ avatar: field.string().nullable().required(),
164
216
  });
217
+ ```
165
218
 
166
- type User = InferInput<typeof UserSchema>;
219
+ ### Enhanced Error Context
220
+
221
+ Get detailed error information:
222
+
223
+ ```typescript
224
+ const result = validate(schema({ age: field.number().min(18) }), { age: 15 });
225
+
226
+ result.hardErrors[0];
167
227
  // {
168
- // email: string;
169
- // age: number;
170
- // tags: string[];
228
+ // field: "age",
229
+ // code: "MIN_VALUE",
230
+ // message: "Value must be at least 18",
231
+ // severity: "hard",
232
+ // received: 15, // ✨ The actual value
233
+ // expected: { min: 18 } // ✨ The constraint that failed
234
+ // path: ["age"] // ✨ Path as array
171
235
  // }
172
236
  ```
173
237
 
174
- ## API Reference
175
-
176
- ### Schema Builders
238
+ ## 📚 All Validators (v1.2.0)
177
239
 
178
- #### `field.string()`
240
+ ### String Validators (17)
179
241
 
180
242
  ```typescript
181
243
  field.string()
182
- .min(length, message?) // Minimum length
183
- .max(length, message?) // Maximum length
184
- .email(message?) // Email format
185
- .url(message?) // URL format
186
- .ipAddress(message?) // IPv4 address format (validates 0-255 range)
187
- .pattern(regex, message?) // Regex pattern
188
- .enum(values, message?) // Enum values
189
- .matches(field, message?) // Match another field
190
- .required(message?) // Mark as required
191
- .optional() // Mark as optional
192
- // Soft versions (warnings only)
193
- .minSoft(length, message?)
194
- .maxSoft(length, message?)
244
+ // Basic
245
+ .required() // Required field
246
+ .min(5) // Min length
247
+ .max(100) // Max length
248
+ .length(10) // Exact length
249
+
250
+ // Format validation
251
+ .email() // Valid email
252
+ .url() // Valid URL
253
+ .ipAddress() // IPv4 (validates 0-255)
254
+ .ipv6() // IPv6 address
255
+
256
+ // Character validation
257
+ .alpha() // Only letters (a-zA-Z)
258
+ .alphanumeric() // Letters + numbers
259
+ .numeric() // Only digits
260
+ .lowercase() // Must be lowercase
261
+ .uppercase() // Must be UPPERCASE
262
+
263
+ // Pattern validation
264
+ .slug() // URL-friendly slug
265
+ .hex() // Hexadecimal
266
+ .base64() // Base64 encoded
267
+ .json() // Valid JSON string
268
+ .pattern(/regex/) // Custom regex
269
+
270
+ // Content validation
271
+ .contains('substring') // Must contain text
272
+ .startsWith('prefix') // Must start with
273
+ .endsWith('suffix') // Must end with
274
+ ```
275
+
276
+ **Examples:**
277
+ ```typescript
278
+ // Email with custom message
279
+ email: field.string().email('Please enter a valid email')
280
+
281
+ // Alphanumeric username
282
+ username: field.string()
283
+ .alphanumeric('Only letters and numbers')
284
+ .min(3)
285
+ .max(20)
286
+
287
+ // URL slug
288
+ slug: field.string()
289
+ .slug('Must be URL-friendly')
290
+ .lowercase()
291
+
292
+ // Hex color
293
+ color: field.string()
294
+ .hex('Invalid color code')
295
+ .length(6)
195
296
  ```
196
297
 
197
- #### `field.number()`
298
+ ### Number Validators (11)
198
299
 
199
300
  ```typescript
200
301
  field.number()
201
- .min(value, message?) // Minimum value
202
- .max(value, message?) // Maximum value
203
- .integer(message?) // Must be integer
204
- .positive(message?) // Must be positive
205
- .negative(message?) // Must be negative
206
- .required(message?)
207
- // Soft versions
208
- .minSoft(value, message?) // or .warnBelow()
209
- .maxSoft(value, message?) // or .warnAbove()
302
+ // Range validation
303
+ .min(0) // Minimum value
304
+ .max(100) // Maximum value
305
+ .between(10, 20) // Between range
306
+
307
+ // Type validation
308
+ .integer() // Must be integer
309
+ .positive() // Must be > 0
310
+ .negative() // Must be < 0
311
+ .even() // Even number
312
+ .odd() // Odd number
313
+ .safe() // Safe integer
314
+ .finite() // Not Infinity/NaN
315
+
316
+ // Special formats
317
+ .port() // Port (0-65535)
318
+ .latitude() // Latitude (-90 to 90)
319
+ .longitude() // Longitude (-180 to 180)
320
+ .percentage() // Percentage (0-100)
321
+
322
+ // Mathematical
323
+ .divisibleBy(5) // Divisible by N
324
+ .multipleOf(3) // Multiple of N
325
+ ```
326
+
327
+ **Examples:**
328
+ ```typescript
329
+ // Port number
330
+ port: field.number()
331
+ .port('Invalid port number')
332
+
333
+ // GPS coordinates
334
+ location: schema({
335
+ latitude: field.number().latitude(),
336
+ longitude: field.number().longitude()
337
+ })
338
+
339
+ // Age with soft warning
340
+ age: field.number()
341
+ .min(13, 'Must be 13+') // Hard error
342
+ .minSoft(18, 'Parental consent') // Soft warning
343
+
344
+ // Even page count
345
+ pages: field.number()
346
+ .integer()
347
+ .even('Must be even number')
348
+ ```
349
+
350
+ ### Date Validators (10)
351
+
352
+ ```typescript
353
+ field.date()
354
+ // Basic
355
+ .after(date) // After date
356
+ .before(date) // Before date
357
+ .past() // Must be in past
358
+ .future() // Must be in future
359
+
360
+ // Relative validation
361
+ .today() // Must be today
362
+ .yesterday() // Must be yesterday
363
+ .tomorrow() // Must be tomorrow
364
+ .thisWeek() // This week
365
+ .thisMonth() // This month
366
+ .thisYear() // This year
367
+
368
+ // Day validation
369
+ .weekday() // Monday-Friday
370
+ .weekend() // Saturday-Sunday
371
+
372
+ // Age validation
373
+ .age(min, max) // Age range from birthdate
374
+ .between(start, end) // Between two dates
375
+ ```
376
+
377
+ **Examples:**
378
+ ```typescript
379
+ // Birth date (18-65 years old)
380
+ birthDate: field.date()
381
+ .age(18, 65, 'Must be 18-65 years old')
382
+ .past('Cannot be in future')
383
+
384
+ // Event must be in future
385
+ eventDate: field.date()
386
+ .future('Event must be scheduled ahead')
387
+ .weekday('Events only on weekdays')
388
+
389
+ // Today's attendance
390
+ checkIn: field.date()
391
+ .today('Must check in today')
392
+ ```
393
+
394
+ ### Array Validators (6)
395
+
396
+ ```typescript
397
+ field.array()
398
+ // Size validation
399
+ .min(2) // Min items
400
+ .max(10) // Max items
401
+ .unique() // All items unique
402
+
403
+ // Content validation
404
+ .includes(item) // Must include item
405
+ .excludes(item) // Must not include item
406
+ .notEmpty() // At least 1 item
407
+ .empty() // Must be empty
408
+
409
+ // Order validation
410
+ .sorted('asc') // Sorted ascending
411
+ .sorted('desc') // Sorted descending
412
+
413
+ // Quality validation
414
+ .compact() // No falsy values
415
+ ```
416
+
417
+ **Examples:**
418
+ ```typescript
419
+ // Tags (1-5 unique items)
420
+ tags: field.array(field.string())
421
+ .min(1, 'At least one tag')
422
+ .max(5, 'Max 5 tags')
423
+ .unique('Tags must be unique')
424
+
425
+ // Must include required item
426
+ permissions: field.array()
427
+ .includes('read', 'Read permission required')
428
+
429
+ // Sorted numbers
430
+ scores: field.array(field.number())
431
+ .sorted('desc', 'Must be sorted highest first')
210
432
  ```
211
433
 
212
- #### `field.boolean()`
434
+ ### Boolean Validators
213
435
 
214
436
  ```typescript
215
437
  field.boolean()
216
- .isTrue(message?) // Must be true
217
- .isFalse(message?) // Must be false
218
- .required(message?)
438
+ .isTrue() // Must be true
439
+ .isFalse() // Must be false
440
+ ```
441
+
442
+ **Examples:**
443
+ ```typescript
444
+ // Terms acceptance
445
+ acceptTerms: field.boolean()
446
+ .isTrue('You must accept terms')
447
+ .required()
448
+
449
+ // Optional newsletter
450
+ newsletter: field.boolean()
451
+ .optional()
219
452
  ```
220
453
 
221
- #### `field.date()`
454
+ ### Object Validators (Nested)
222
455
 
223
456
  ```typescript
224
- field.date()
225
- .after(date, message?) // After a date
226
- .before(date, message?) // Before a date
227
- .past(message?) // Must be in past
228
- .future(message?) // Must be in future
229
- .required(message?)
457
+ field.object(schema) // Nested schema validation
458
+ ```
459
+
460
+ **Examples:**
461
+ ```typescript
462
+ // Nested address
463
+ const AddressSchema = schema({
464
+ street: field.string().required(),
465
+ city: field.string().required(),
466
+ zipCode: field.string().pattern(/^\d{5}$/)
467
+ });
468
+
469
+ const UserSchema = schema({
470
+ name: field.string().required(),
471
+ address: field.object(AddressSchema).required()
472
+ });
230
473
  ```
231
474
 
232
- #### `field.array(itemBuilder?)`
475
+ ### Cross-Field Validators (5)
233
476
 
234
477
  ```typescript
235
- field.array(field.string())
236
- .min(count, message?) // Minimum items
237
- .max(count, message?) // Maximum items
238
- .length(count, message?) // Exact count
239
- .unique(message?) // All items unique
240
- .required(message?)
478
+ field.string()
479
+ .matches('password') // Must match field
480
+ .notMatches('oldPassword') // Must NOT match field
481
+
482
+ field.number()
483
+ .greaterThan('minValue') // > another field
484
+ .lessThan('maxValue') // < another field
485
+
486
+ field.string()
487
+ .dependsOn('country') // Required if field exists
488
+ ```
489
+
490
+ **Examples:**
491
+ ```typescript
492
+ // Password confirmation
493
+ const schema = schema({
494
+ password: field.string().min(8),
495
+ confirmPassword: field.string()
496
+ .matches('password', 'Passwords must match')
497
+ });
498
+
499
+ // New password must differ
500
+ const changePasswordSchema = schema({
501
+ currentPassword: field.string(),
502
+ newPassword: field.string()
503
+ .notMatches('currentPassword', 'Must be different')
504
+ });
505
+
506
+ // Range validation
507
+ const rangeSchema = schema({
508
+ minPrice: field.number(),
509
+ maxPrice: field.number()
510
+ .greaterThan('minPrice', 'Max must be > min')
511
+ });
512
+
513
+ // Conditional requirement
514
+ const locationSchema = schema({
515
+ country: field.string(),
516
+ state: field.string()
517
+ .dependsOn('country', 'State requires country')
518
+ });
241
519
  ```
242
520
 
243
- #### `field.object(schema)`
521
+ ## 💡 Hard vs Soft Validation
522
+
523
+ Unischema supports two-tier validation for enterprise applications:
244
524
 
245
525
  ```typescript
246
- field.object(AddressSchema)
247
- .required(message?)
526
+ const TransactionSchema = schema({
527
+ amount: field.number()
528
+ .min(0.01, 'Amount must be positive') // ❌ Hard: blocks submission
529
+ .maxSoft(10000, 'Review required for $10k+') // ⚠️ Soft: warning only
530
+ });
531
+
532
+ const result = validateSchema(TransactionSchema.definition, { amount: 15000 });
533
+
534
+ console.log(result.valid); // true (no hard errors)
535
+ console.log(result.hardErrors); // []
536
+ console.log(result.softErrors); // [{ field: 'amount', message: 'Review required...', severity: 'soft' }]
248
537
  ```
249
538
 
250
- ### Validation Functions
539
+ **Use cases:**
540
+ - Warnings that don't block submission
541
+ - Age warnings (13+ required, 18+ recommended)
542
+ - Security score suggestions
543
+ - Large transaction reviews
544
+
545
+ ## 🔧 Advanced Usage
546
+
547
+ ### Schema Composition
251
548
 
252
549
  ```typescript
253
- import { validate, isValid, assertValid } from 'formschema';
550
+ // Extend schemas
551
+ const BaseUser = schema({
552
+ email: field.string().email(),
553
+ name: field.string()
554
+ });
555
+
556
+ const AdminUser = extend(BaseUser, {
557
+ role: field.string().enum(['admin', 'superadmin']),
558
+ permissions: field.array(field.string())
559
+ });
254
560
 
255
- // Returns ValidationResult
256
- const result = validate(schema.definition, data);
257
- // { valid: boolean, hardErrors: [], softErrors: [] }
561
+ // Pick specific fields
562
+ const LoginSchema = pick(BaseUser, ['email']);
258
563
 
259
- // Returns boolean
260
- const valid = isValid(schema.definition, data);
564
+ // Omit fields
565
+ const PublicUser = omit(BaseUser, ['password']);
261
566
 
262
- // Throws if invalid
263
- const data = assertValid(schema.definition, input);
567
+ // Merge schemas
568
+ const FullSchema = merge(ProfileSchema, SettingsSchema);
264
569
  ```
265
570
 
266
- ### Backend Adapters
571
+ ### TypeScript Integration
267
572
 
268
573
  ```typescript
269
- import {
270
- validateBody,
271
- validateQuery,
272
- validateParams,
273
- withValidation,
274
- createHandler,
275
- } from 'formschema/backend';
276
-
277
- // Express middleware
278
- app.post('/users', validateBody(UserSchema), handler);
279
- app.get('/users', validateQuery(QuerySchema), handler);
280
- app.get('/users/:id', validateParams(ParamsSchema), handler);
574
+ import { type InferInput, type InferOutput } from 'unischema';
575
+
576
+ const UserSchema = schema({
577
+ email: field.string().email().required(),
578
+ age: field.number().min(0)
579
+ });
580
+
581
+ // Input type (what you pass in)
582
+ type UserInput = InferInput<typeof UserSchema>;
583
+ // { email: string; age: number }
281
584
 
282
- // Wrapper with typed handler
283
- app.post('/users', ...withValidation(UserSchema, async (req, res) => {
284
- const data = req.validatedData; // Typed!
285
- }));
585
+ // Output type (after validation)
586
+ type UserOutput = InferOutput<typeof UserSchema>;
587
+ ```
588
+
589
+ ### Custom Validation
286
590
 
287
- // Serverless handler
288
- const handler = createHandler(UserSchema, async ({ data }) => {
289
- return { user: await createUser(data) };
591
+ ```typescript
592
+ const schema = schema({
593
+ password: field.string()
594
+ .custom((value, context) => {
595
+ if (!/[A-Z]/.test(value)) {
596
+ return { valid: false, message: 'Need uppercase letter' };
597
+ }
598
+ return true;
599
+ })
290
600
  });
291
601
  ```
292
602
 
293
- ### Frontend Adapters
603
+ ### Granular Imports (Tree-Shaking)
294
604
 
295
605
  ```typescript
296
- import { createForm, parseApiErrors, focusFirstError } from 'formschema/frontend';
606
+ // Import only what you need
607
+ import { emailValidator } from 'unischema/validators/string';
608
+ import { portValidator } from 'unischema/validators/number';
609
+ import { todayValidator } from 'unischema/validators/date';
610
+
611
+ // Or import by category
612
+ import * as stringValidators from 'unischema/validators/string';
613
+ import * as numberValidators from 'unischema/validators/number';
614
+ ```
615
+
616
+ ## 🌐 Framework Examples
617
+
618
+ ### React
619
+
620
+ ```tsx
621
+ import { createForm } from 'unischema/frontend';
622
+ import { UserSchema } from './schemas';
623
+
624
+ function RegisterForm() {
625
+ const form = createForm(UserSchema, {
626
+ initialValues: { email: '', password: '' },
627
+ onSubmit: async (values) => {
628
+ await api.register(values);
629
+ }
630
+ });
631
+
632
+ const emailProps = form.getFieldProps('email');
633
+
634
+ return (
635
+ <form onSubmit={form.handleSubmit}>
636
+ <input {...emailProps} />
637
+ {emailProps.hasError && <span>{emailProps.error}</span>}
638
+
639
+ <button type="submit">Register</button>
640
+ </form>
641
+ );
642
+ }
643
+ ```
644
+
645
+ ### Vue
646
+
647
+ ```vue
648
+ <script setup>
649
+ import { createForm } from 'unischema/frontend';
650
+ import { UserSchema } from './schemas';
297
651
 
298
652
  const form = createForm(UserSchema, {
299
- initialValues: { email: '', password: '' },
300
- validateOnChange: true,
301
- validateOnBlur: true,
302
- onSubmit: async (values, helpers) => {
303
- // Submit logic
304
- },
653
+ onSubmit: async (values) => {
654
+ await api.register(values);
655
+ }
305
656
  });
306
657
 
307
- // Form methods
308
- form.setFieldValue('email', 'test@example.com');
309
- form.touchField('email');
310
- form.validate();
311
- form.validateField('email');
312
- form.reset();
313
- form.handleSubmit();
658
+ const emailProps = form.getFieldProps('email');
659
+ </script>
660
+
661
+ <template>
662
+ <form @submit.prevent="form.handleSubmit">
663
+ <input v-bind="emailProps" />
664
+ <span v-if="emailProps.hasError">{{ emailProps.error }}</span>
665
+ </form>
666
+ </template>
667
+ ```
314
668
 
315
- // Get field props for binding
316
- const props = form.getFieldProps('email');
317
- // { name, value, onChange, onBlur, error, hasError, warning, hasWarning, ... }
669
+ ### Express.js
318
670
 
319
- // Handle server errors
320
- const result = parseApiErrors(apiResponse);
321
- form.setServerErrors(result.hardErrors);
671
+ ```typescript
672
+ import express from 'express';
673
+ import { validateBody, validateQuery, validateParams } from 'unischema/backend';
674
+
675
+ const app = express();
676
+
677
+ // Body validation
678
+ app.post('/users', validateBody(UserSchema), (req, res) => {
679
+ const user = req.validatedData; // ✅ Typed and validated
680
+ res.json(user);
681
+ });
682
+
683
+ // Query validation
684
+ app.get('/search', validateQuery(SearchSchema), (req, res) => {
685
+ const { query } = req.validatedData;
686
+ res.json(results);
687
+ });
688
+
689
+ // Params validation
690
+ app.get('/users/:id', validateParams(IdSchema), (req, res) => {
691
+ const { id } = req.validatedData;
692
+ res.json(user);
693
+ });
322
694
  ```
323
695
 
324
- ## Enterprise Response Format
696
+ ## 📊 Real-World Examples
325
697
 
326
- FormSchema uses a consistent error structure compatible with enterprise systems:
698
+ ### User Registration
327
699
 
328
700
  ```typescript
329
- interface EnterpriseValidationResponse {
330
- status: 'success' | 'validation_error';
331
- data?: unknown;
332
- errors: ValidationError[];
333
- msg: string;
334
- validation: {
335
- hard_validations: ValidationError[];
336
- soft_validations: ValidationError[];
337
- };
338
- }
701
+ const RegisterSchema = schema({
702
+ email: field.string()
703
+ .email('Invalid email address')
704
+ .required('Email is required'),
339
705
 
340
- interface ValidationError {
341
- field: string; // e.g., "email" or "address.city"
342
- code: string; // e.g., "REQUIRED", "MIN_LENGTH"
343
- message: string; // Human-readable message
344
- severity: 'hard' | 'soft';
345
- }
706
+ username: field.string()
707
+ .alphanumeric('Only letters and numbers')
708
+ .min(3, 'At least 3 characters')
709
+ .max(20, 'Max 20 characters')
710
+ .required(),
711
+
712
+ password: field.string()
713
+ .min(8, 'At least 8 characters')
714
+ .pattern(/[A-Z]/, 'Need uppercase letter')
715
+ .pattern(/[0-9]/, 'Need a number')
716
+ .required(),
717
+
718
+ confirmPassword: field.string()
719
+ .matches('password', 'Passwords must match')
720
+ .required(),
721
+
722
+ age: field.number()
723
+ .min(13, 'Must be 13+')
724
+ .minSoft(18, 'Parental consent required under 18')
725
+ .max(120, 'Invalid age')
726
+ .integer()
727
+ .required(),
728
+
729
+ acceptTerms: field.boolean()
730
+ .isTrue('You must accept the terms')
731
+ .required()
732
+ });
346
733
  ```
347
734
 
348
- ## Incremental Adoption
735
+ ### E-Commerce Order
349
736
 
350
- FormSchema supports incremental adoption in existing systems:
737
+ ```typescript
738
+ const OrderSchema = schema({
739
+ customerId: field.string()
740
+ .alphanumeric()
741
+ .length(10)
742
+ .required(),
743
+
744
+ items: field.array(field.object(schema({
745
+ productId: field.string().required(),
746
+ quantity: field.number().min(1).integer(),
747
+ price: field.number().positive()
748
+ })))
749
+ .min(1, 'At least one item required')
750
+ .max(50, 'Maximum 50 items per order'),
751
+
752
+ total: field.number()
753
+ .positive()
754
+ .required(),
755
+
756
+ shippingAddress: field.object(schema({
757
+ street: field.string().required(),
758
+ city: field.string().required(),
759
+ state: field.string().uppercase().length(2),
760
+ zipCode: field.string().pattern(/^\d{5}$/)
761
+ })).required(),
762
+
763
+ shippingDate: field.date()
764
+ .future('Must be a future date')
765
+ .weekday('No weekend shipping')
766
+ });
767
+ ```
768
+
769
+ ### API Configuration
351
770
 
352
771
  ```typescript
353
- // Use only backend validation
354
- import { validateInput } from 'formschema/backend';
355
- const { valid, data, response } = validateInput(UserSchema, input);
772
+ const ServerConfigSchema = schema({
773
+ host: field.string()
774
+ .ipAddress('Invalid IP address')
775
+ .required(),
776
+
777
+ port: field.number()
778
+ .port('Invalid port number')
779
+ .required(),
780
+
781
+ ssl: field.boolean()
782
+ .required(),
783
+
784
+ maxConnections: field.number()
785
+ .integer()
786
+ .positive()
787
+ .between(1, 10000),
788
+
789
+ timeout: field.number()
790
+ .integer()
791
+ .positive()
792
+ .multipleOf(1000, 'Must be in seconds (1000ms)')
793
+ });
794
+ ```
356
795
 
357
- // Use only type generation
358
- import { type InferInput } from 'formschema';
359
- type User = InferInput<typeof UserSchema>;
796
+ ## 🚀 Migration Guide
797
+
798
+ ### From Yup
799
+
800
+ ```typescript
801
+ // Yup
802
+ const schema = yup.object({
803
+ email: yup.string().email().required(),
804
+ age: yup.number().min(18)
805
+ });
806
+
807
+ // Unischema
808
+ const schema = schema({
809
+ email: field.string().email().required(),
810
+ age: field.number().min(18)
811
+ });
812
+ ```
813
+
814
+ ### From Zod
815
+
816
+ ```typescript
817
+ // Zod
818
+ const schema = z.object({
819
+ email: z.string().email(),
820
+ age: z.number().min(18)
821
+ });
360
822
 
361
- // Use only frontend validation
362
- import { validate } from 'formschema';
363
- const result = validate(UserSchema.definition, formData);
823
+ // Unischema
824
+ const schema = schema({
825
+ email: field.string().email(),
826
+ age: field.number().min(18)
827
+ });
828
+ ```
829
+
830
+ ## 🎨 Bundle Size
831
+
832
+ Unischema is optimized for tree-shaking:
833
+
834
+ - **Full library**: ~15KB min+gzip
835
+ - **Core only**: ~5KB min+gzip
836
+ - **Single validator**: ~2KB min+gzip
837
+
838
+ Import only what you use for minimal bundle size.
839
+
840
+ ## 📖 API Reference
841
+
842
+ ### Core Functions
843
+
844
+ ```typescript
845
+ import {
846
+ // Schema creation
847
+ schema, // Create schema
848
+ field, // Field builders
849
+ coerce, // Type coercion builders
850
+
851
+ // Sync validation
852
+ validate, // Validate data
853
+ validateSchema, // Validate with schema
854
+ isValid, // Boolean validation
855
+ assertValid, // Throws if invalid
856
+
857
+ // Async validation (v1.2.0)
858
+ validateAsync, // Async validate data
859
+ validateSchemaAsync, // Async validate with schema
860
+ isValidAsync, // Async boolean validation
861
+ assertValidAsync, // Async throws if invalid
862
+
863
+ // Schema composition
864
+ extend, // Extend schema
865
+ pick, // Pick fields
866
+ omit, // Omit fields
867
+ merge, // Merge schemas
868
+ partial, // Make all optional
869
+ deepPartial, // Make all optional recursively (v1.2.0)
870
+ passthrough, // Allow unknown keys (v1.2.0)
871
+ strict, // Reject unknown keys (v1.2.0)
872
+ catchall, // Validate unknown keys (v1.2.0)
873
+ required, // Make specific fields required (v1.2.0)
874
+ optional, // Make specific fields optional (v1.2.0)
875
+
876
+ // Type inference
877
+ type InferInput, // Input type
878
+ type InferOutput // Output type
879
+ } from 'unischema';
364
880
  ```
365
881
 
366
- ## License
882
+ ### Backend
883
+
884
+ ```typescript
885
+ import {
886
+ validateBody, // Validate request body
887
+ validateQuery, // Validate query params
888
+ validateParams, // Validate route params
889
+ withValidation, // Wrapper with validation
890
+ createHandler // Serverless handler
891
+ } from 'unischema/backend';
892
+ ```
893
+
894
+ ### Frontend
895
+
896
+ ```typescript
897
+ import {
898
+ createForm, // Create form helper
899
+ parseApiErrors, // Parse server errors
900
+ focusFirstError // Focus first error field
901
+ } from 'unischema/frontend';
902
+ ```
903
+
904
+ ## 🤝 Contributing
905
+
906
+ Contributions are welcome! Please check out the [GitHub repository](https://github.com/Gaurav-pasi/unischema).
907
+
908
+ ## 📄 License
909
+
910
+ MIT © [Gaurav Pasi](https://github.com/Gaurav-pasi)
911
+
912
+ ## 🔗 Links
913
+
914
+ - [npm package](https://www.npmjs.com/package/unischema)
915
+ - [GitHub repository](https://github.com/Gaurav-pasi/unischema)
916
+ - [Issue tracker](https://github.com/Gaurav-pasi/unischema/issues)
917
+
918
+ ---
367
919
 
368
- MIT
920
+ **Made with ❤️ for developers who value type safety and code reusability**