unischema 1.0.1 → 1.1.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 +634 -231
  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-2JYFKT3R.js +103 -0
  11. package/dist/chunk-3FANCMEF.js +206 -0
  12. package/dist/chunk-3TS35CVJ.mjs +478 -0
  13. package/dist/chunk-ASKTY6EG.js +131 -0
  14. package/dist/chunk-BJLVOIAP.js +491 -0
  15. package/dist/chunk-BNIB23NQ.js +90 -0
  16. package/dist/chunk-BVRXGZLS.js +17 -0
  17. package/dist/chunk-CQYXR2LZ.js +353 -0
  18. package/dist/chunk-ELL7U7IC.mjs +237 -0
  19. package/dist/chunk-FKDWSZIV.mjs +39 -0
  20. package/dist/chunk-FRBZHN4K.mjs +335 -0
  21. package/dist/chunk-FZ7K2PC7.js +248 -0
  22. package/dist/chunk-KHHJD6QK.mjs +85 -0
  23. package/dist/chunk-NUW55QTO.js +48 -0
  24. package/dist/chunk-TTK77YBI.mjs +15 -0
  25. package/dist/chunk-VWP24NYS.mjs +194 -0
  26. package/dist/chunk-XC4DKEXP.mjs +97 -0
  27. package/dist/chunk-XGTUU27F.mjs +124 -0
  28. package/dist/index-BQR7OrY7.d.mts +80 -0
  29. package/dist/index-BQR7OrY7.d.ts +80 -0
  30. package/dist/index.d.mts +3 -2
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.js +527 -494
  33. package/dist/index.mjs +476 -482
  34. package/dist/{schema-D9DGC9E_.d.mts → schema-CpAjXgEF.d.ts} +182 -79
  35. package/dist/{schema-D9DGC9E_.d.ts → schema-DYU1zGVm.d.mts} +182 -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 +36 -1
  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,771 @@
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);
24
29
 
25
- ## Installation
30
+ // ✅ Use on frontend (any framework)
31
+ const form = createForm(UserSchema, { onSubmit });
32
+
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
+ - ✅ **50+ Built-in Validators** - Email, URL, IPv4/IPv6, phone, coordinates, and more
102
+ - ✅ **Isomorphic** - Same code runs in browser and Node.js
103
+ - ✅ **TypeScript First** - Automatic type inference
104
+ - ✅ **Hard & Soft Validation** - Errors vs warnings for enterprise apps
105
+ - ✅ **Tree-Shakeable** - Only bundle what you use (~2KB min+gzip)
106
+ - ✅ **Framework Agnostic** - Works with React, Vue, Svelte, Angular, etc.
107
+ - ✅ **Zero Dependencies** - Lightweight and fast
108
108
 
109
- ### Hard vs Soft Validation
109
+ ## 📚 All Validators (v1.1.0)
110
110
 
111
- FormSchema supports two-tier validation, essential for enterprise applications:
111
+ ### String Validators (17)
112
112
 
113
113
  ```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
118
- });
114
+ field.string()
115
+ // Basic
116
+ .required() // Required field
117
+ .min(5) // Min length
118
+ .max(100) // Max length
119
+ .length(10) // Exact length
120
+
121
+ // Format validation
122
+ .email() // Valid email
123
+ .url() // Valid URL
124
+ .ipAddress() // IPv4 (validates 0-255)
125
+ .ipv6() // IPv6 address
126
+
127
+ // Character validation
128
+ .alpha() // Only letters (a-zA-Z)
129
+ .alphanumeric() // Letters + numbers
130
+ .numeric() // Only digits
131
+ .lowercase() // Must be lowercase
132
+ .uppercase() // Must be UPPERCASE
133
+
134
+ // Pattern validation
135
+ .slug() // URL-friendly slug
136
+ .hex() // Hexadecimal
137
+ .base64() // Base64 encoded
138
+ .json() // Valid JSON string
139
+ .pattern(/regex/) // Custom regex
140
+
141
+ // Content validation
142
+ .contains('substring') // Must contain text
143
+ .startsWith('prefix') // Must start with
144
+ .endsWith('suffix') // Must end with
145
+ ```
146
+
147
+ **Examples:**
148
+ ```typescript
149
+ // Email with custom message
150
+ email: field.string().email('Please enter a valid email')
151
+
152
+ // Alphanumeric username
153
+ username: field.string()
154
+ .alphanumeric('Only letters and numbers')
155
+ .min(3)
156
+ .max(20)
157
+
158
+ // URL slug
159
+ slug: field.string()
160
+ .slug('Must be URL-friendly')
161
+ .lowercase()
162
+
163
+ // Hex color
164
+ color: field.string()
165
+ .hex('Invalid color code')
166
+ .length(6)
167
+ ```
119
168
 
120
- const result = validate(TransactionSchema.definition, { amount: 50000 });
169
+ ### Number Validators (11)
121
170
 
122
- result.valid; // true - no hard errors
123
- result.hardErrors; // []
124
- result.softErrors; // [{ field: 'amount', message: 'Large transaction...', severity: 'soft' }]
171
+ ```typescript
172
+ field.number()
173
+ // Range validation
174
+ .min(0) // Minimum value
175
+ .max(100) // Maximum value
176
+ .between(10, 20) // Between range
177
+
178
+ // Type validation
179
+ .integer() // Must be integer
180
+ .positive() // Must be > 0
181
+ .negative() // Must be < 0
182
+ .even() // Even number
183
+ .odd() // Odd number
184
+ .safe() // Safe integer
185
+ .finite() // Not Infinity/NaN
186
+
187
+ // Special formats
188
+ .port() // Port (0-65535)
189
+ .latitude() // Latitude (-90 to 90)
190
+ .longitude() // Longitude (-180 to 180)
191
+ .percentage() // Percentage (0-100)
192
+
193
+ // Mathematical
194
+ .divisibleBy(5) // Divisible by N
195
+ .multipleOf(3) // Multiple of N
125
196
  ```
126
197
 
127
- **Hard validations** block form submission. **Soft validations** are warnings that don't block.
198
+ **Examples:**
199
+ ```typescript
200
+ // Port number
201
+ port: field.number()
202
+ .port('Invalid port number')
203
+
204
+ // GPS coordinates
205
+ location: schema({
206
+ latitude: field.number().latitude(),
207
+ longitude: field.number().longitude()
208
+ })
209
+
210
+ // Age with soft warning
211
+ age: field.number()
212
+ .min(13, 'Must be 13+') // Hard error
213
+ .minSoft(18, 'Parental consent') // Soft warning
214
+
215
+ // Even page count
216
+ pages: field.number()
217
+ .integer()
218
+ .even('Must be even number')
219
+ ```
128
220
 
129
- ### Schema Composition
221
+ ### Date Validators (10)
130
222
 
131
- Build complex schemas from reusable parts:
223
+ ```typescript
224
+ field.date()
225
+ // Basic
226
+ .after(date) // After date
227
+ .before(date) // Before date
228
+ .past() // Must be in past
229
+ .future() // Must be in future
230
+
231
+ // Relative validation
232
+ .today() // Must be today
233
+ .yesterday() // Must be yesterday
234
+ .tomorrow() // Must be tomorrow
235
+ .thisWeek() // This week
236
+ .thisMonth() // This month
237
+ .thisYear() // This year
238
+
239
+ // Day validation
240
+ .weekday() // Monday-Friday
241
+ .weekend() // Saturday-Sunday
242
+
243
+ // Age validation
244
+ .age(min, max) // Age range from birthdate
245
+ .between(start, end) // Between two dates
246
+ ```
247
+
248
+ **Examples:**
249
+ ```typescript
250
+ // Birth date (18-65 years old)
251
+ birthDate: field.date()
252
+ .age(18, 65, 'Must be 18-65 years old')
253
+ .past('Cannot be in future')
254
+
255
+ // Event must be in future
256
+ eventDate: field.date()
257
+ .future('Event must be scheduled ahead')
258
+ .weekday('Events only on weekdays')
259
+
260
+ // Today's attendance
261
+ checkIn: field.date()
262
+ .today('Must check in today')
263
+ ```
264
+
265
+ ### Array Validators (6)
132
266
 
133
267
  ```typescript
268
+ field.array()
269
+ // Size validation
270
+ .min(2) // Min items
271
+ .max(10) // Max items
272
+ .unique() // All items unique
273
+
274
+ // Content validation
275
+ .includes(item) // Must include item
276
+ .excludes(item) // Must not include item
277
+ .notEmpty() // At least 1 item
278
+ .empty() // Must be empty
279
+
280
+ // Order validation
281
+ .sorted('asc') // Sorted ascending
282
+ .sorted('desc') // Sorted descending
283
+
284
+ // Quality validation
285
+ .compact() // No falsy values
286
+ ```
287
+
288
+ **Examples:**
289
+ ```typescript
290
+ // Tags (1-5 unique items)
291
+ tags: field.array(field.string())
292
+ .min(1, 'At least one tag')
293
+ .max(5, 'Max 5 tags')
294
+ .unique('Tags must be unique')
295
+
296
+ // Must include required item
297
+ permissions: field.array()
298
+ .includes('read', 'Read permission required')
299
+
300
+ // Sorted numbers
301
+ scores: field.array(field.number())
302
+ .sorted('desc', 'Must be sorted highest first')
303
+ ```
304
+
305
+ ### Boolean Validators
306
+
307
+ ```typescript
308
+ field.boolean()
309
+ .isTrue() // Must be true
310
+ .isFalse() // Must be false
311
+ ```
312
+
313
+ **Examples:**
314
+ ```typescript
315
+ // Terms acceptance
316
+ acceptTerms: field.boolean()
317
+ .isTrue('You must accept terms')
318
+ .required()
319
+
320
+ // Optional newsletter
321
+ newsletter: field.boolean()
322
+ .optional()
323
+ ```
324
+
325
+ ### Object Validators (Nested)
326
+
327
+ ```typescript
328
+ field.object(schema) // Nested schema validation
329
+ ```
330
+
331
+ **Examples:**
332
+ ```typescript
333
+ // Nested address
134
334
  const AddressSchema = schema({
135
335
  street: field.string().required(),
136
336
  city: field.string().required(),
137
- zipCode: field.string().pattern(/^\d{5}$/),
337
+ zipCode: field.string().pattern(/^\d{5}$/)
138
338
  });
139
339
 
140
340
  const UserSchema = schema({
141
341
  name: field.string().required(),
142
- address: field.object(AddressSchema).required(),
342
+ address: field.object(AddressSchema).required()
143
343
  });
344
+ ```
144
345
 
145
- // Extend schemas
146
- const AdminSchema = extend(UserSchema, {
147
- role: field.string().enum(['admin', 'superadmin']),
346
+ ### Cross-Field Validators (5)
347
+
348
+ ```typescript
349
+ field.string()
350
+ .matches('password') // Must match field
351
+ .notMatches('oldPassword') // Must NOT match field
352
+
353
+ field.number()
354
+ .greaterThan('minValue') // > another field
355
+ .lessThan('maxValue') // < another field
356
+
357
+ field.string()
358
+ .dependsOn('country') // Required if field exists
359
+ ```
360
+
361
+ **Examples:**
362
+ ```typescript
363
+ // Password confirmation
364
+ const schema = schema({
365
+ password: field.string().min(8),
366
+ confirmPassword: field.string()
367
+ .matches('password', 'Passwords must match')
148
368
  });
149
369
 
150
- // Pick/Omit fields
151
- const PublicUserSchema = omit(UserSchema, ['password']);
152
- const LoginSchema = pick(UserSchema, ['email', 'password']);
370
+ // New password must differ
371
+ const changePasswordSchema = schema({
372
+ currentPassword: field.string(),
373
+ newPassword: field.string()
374
+ .notMatches('currentPassword', 'Must be different')
375
+ });
376
+
377
+ // Range validation
378
+ const rangeSchema = schema({
379
+ minPrice: field.number(),
380
+ maxPrice: field.number()
381
+ .greaterThan('minPrice', 'Max must be > min')
382
+ });
383
+
384
+ // Conditional requirement
385
+ const locationSchema = schema({
386
+ country: field.string(),
387
+ state: field.string()
388
+ .dependsOn('country', 'State requires country')
389
+ });
153
390
  ```
154
391
 
155
- ### Type Inference
392
+ ## 💡 Hard vs Soft Validation
156
393
 
157
- Types are automatically inferred from your schema:
394
+ Unischema supports two-tier validation for enterprise applications:
158
395
 
159
396
  ```typescript
160
- const UserSchema = schema({
161
- email: field.string().email().required(),
162
- age: field.number().min(0),
163
- tags: field.array(field.string()),
397
+ const TransactionSchema = schema({
398
+ amount: field.number()
399
+ .min(0.01, 'Amount must be positive') // ❌ Hard: blocks submission
400
+ .maxSoft(10000, 'Review required for $10k+') // ⚠️ Soft: warning only
164
401
  });
165
402
 
166
- type User = InferInput<typeof UserSchema>;
167
- // {
168
- // email: string;
169
- // age: number;
170
- // tags: string[];
171
- // }
403
+ const result = validateSchema(TransactionSchema.definition, { amount: 15000 });
404
+
405
+ console.log(result.valid); // true (no hard errors)
406
+ console.log(result.hardErrors); // []
407
+ console.log(result.softErrors); // [{ field: 'amount', message: 'Review required...', severity: 'soft' }]
172
408
  ```
173
409
 
174
- ## API Reference
410
+ **Use cases:**
411
+ - Warnings that don't block submission
412
+ - Age warnings (13+ required, 18+ recommended)
413
+ - Security score suggestions
414
+ - Large transaction reviews
175
415
 
176
- ### Schema Builders
416
+ ## 🔧 Advanced Usage
177
417
 
178
- #### `field.string()`
418
+ ### Schema Composition
179
419
 
180
420
  ```typescript
181
- 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?)
421
+ // Extend schemas
422
+ const BaseUser = schema({
423
+ email: field.string().email(),
424
+ name: field.string()
425
+ });
426
+
427
+ const AdminUser = extend(BaseUser, {
428
+ role: field.string().enum(['admin', 'superadmin']),
429
+ permissions: field.array(field.string())
430
+ });
431
+
432
+ // Pick specific fields
433
+ const LoginSchema = pick(BaseUser, ['email']);
434
+
435
+ // Omit fields
436
+ const PublicUser = omit(BaseUser, ['password']);
437
+
438
+ // Merge schemas
439
+ const FullSchema = merge(ProfileSchema, SettingsSchema);
195
440
  ```
196
441
 
197
- #### `field.number()`
442
+ ### TypeScript Integration
198
443
 
199
444
  ```typescript
200
- 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()
445
+ import { type InferInput, type InferOutput } from 'unischema';
446
+
447
+ const UserSchema = schema({
448
+ email: field.string().email().required(),
449
+ age: field.number().min(0)
450
+ });
451
+
452
+ // Input type (what you pass in)
453
+ type UserInput = InferInput<typeof UserSchema>;
454
+ // { email: string; age: number }
455
+
456
+ // Output type (after validation)
457
+ type UserOutput = InferOutput<typeof UserSchema>;
210
458
  ```
211
459
 
212
- #### `field.boolean()`
460
+ ### Custom Validation
213
461
 
214
462
  ```typescript
215
- field.boolean()
216
- .isTrue(message?) // Must be true
217
- .isFalse(message?) // Must be false
218
- .required(message?)
463
+ const schema = schema({
464
+ password: field.string()
465
+ .custom((value, context) => {
466
+ if (!/[A-Z]/.test(value)) {
467
+ return { valid: false, message: 'Need uppercase letter' };
468
+ }
469
+ return true;
470
+ })
471
+ });
219
472
  ```
220
473
 
221
- #### `field.date()`
474
+ ### Granular Imports (Tree-Shaking)
222
475
 
223
476
  ```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?)
477
+ // Import only what you need
478
+ import { emailValidator } from 'unischema/validators/string';
479
+ import { portValidator } from 'unischema/validators/number';
480
+ import { todayValidator } from 'unischema/validators/date';
481
+
482
+ // Or import by category
483
+ import * as stringValidators from 'unischema/validators/string';
484
+ import * as numberValidators from 'unischema/validators/number';
230
485
  ```
231
486
 
232
- #### `field.array(itemBuilder?)`
487
+ ## 🌐 Framework Examples
233
488
 
234
- ```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?)
489
+ ### React
490
+
491
+ ```tsx
492
+ import { createForm } from 'unischema/frontend';
493
+ import { UserSchema } from './schemas';
494
+
495
+ function RegisterForm() {
496
+ const form = createForm(UserSchema, {
497
+ initialValues: { email: '', password: '' },
498
+ onSubmit: async (values) => {
499
+ await api.register(values);
500
+ }
501
+ });
502
+
503
+ const emailProps = form.getFieldProps('email');
504
+
505
+ return (
506
+ <form onSubmit={form.handleSubmit}>
507
+ <input {...emailProps} />
508
+ {emailProps.hasError && <span>{emailProps.error}</span>}
509
+
510
+ <button type="submit">Register</button>
511
+ </form>
512
+ );
513
+ }
241
514
  ```
242
515
 
243
- #### `field.object(schema)`
516
+ ### Vue
244
517
 
245
- ```typescript
246
- field.object(AddressSchema)
247
- .required(message?)
518
+ ```vue
519
+ <script setup>
520
+ import { createForm } from 'unischema/frontend';
521
+ import { UserSchema } from './schemas';
522
+
523
+ const form = createForm(UserSchema, {
524
+ onSubmit: async (values) => {
525
+ await api.register(values);
526
+ }
527
+ });
528
+
529
+ const emailProps = form.getFieldProps('email');
530
+ </script>
531
+
532
+ <template>
533
+ <form @submit.prevent="form.handleSubmit">
534
+ <input v-bind="emailProps" />
535
+ <span v-if="emailProps.hasError">{{ emailProps.error }}</span>
536
+ </form>
537
+ </template>
248
538
  ```
249
539
 
250
- ### Validation Functions
540
+ ### Express.js
251
541
 
252
542
  ```typescript
253
- import { validate, isValid, assertValid } from 'formschema';
543
+ import express from 'express';
544
+ import { validateBody, validateQuery, validateParams } from 'unischema/backend';
254
545
 
255
- // Returns ValidationResult
256
- const result = validate(schema.definition, data);
257
- // { valid: boolean, hardErrors: [], softErrors: [] }
546
+ const app = express();
258
547
 
259
- // Returns boolean
260
- const valid = isValid(schema.definition, data);
548
+ // Body validation
549
+ app.post('/users', validateBody(UserSchema), (req, res) => {
550
+ const user = req.validatedData; // ✅ Typed and validated
551
+ res.json(user);
552
+ });
553
+
554
+ // Query validation
555
+ app.get('/search', validateQuery(SearchSchema), (req, res) => {
556
+ const { query } = req.validatedData;
557
+ res.json(results);
558
+ });
261
559
 
262
- // Throws if invalid
263
- const data = assertValid(schema.definition, input);
560
+ // Params validation
561
+ app.get('/users/:id', validateParams(IdSchema), (req, res) => {
562
+ const { id } = req.validatedData;
563
+ res.json(user);
564
+ });
264
565
  ```
265
566
 
266
- ### Backend Adapters
567
+ ## 📊 Real-World Examples
568
+
569
+ ### User Registration
267
570
 
268
571
  ```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);
572
+ const RegisterSchema = schema({
573
+ email: field.string()
574
+ .email('Invalid email address')
575
+ .required('Email is required'),
576
+
577
+ username: field.string()
578
+ .alphanumeric('Only letters and numbers')
579
+ .min(3, 'At least 3 characters')
580
+ .max(20, 'Max 20 characters')
581
+ .required(),
281
582
 
282
- // Wrapper with typed handler
283
- app.post('/users', ...withValidation(UserSchema, async (req, res) => {
284
- const data = req.validatedData; // Typed!
285
- }));
583
+ password: field.string()
584
+ .min(8, 'At least 8 characters')
585
+ .pattern(/[A-Z]/, 'Need uppercase letter')
586
+ .pattern(/[0-9]/, 'Need a number')
587
+ .required(),
588
+
589
+ confirmPassword: field.string()
590
+ .matches('password', 'Passwords must match')
591
+ .required(),
286
592
 
287
- // Serverless handler
288
- const handler = createHandler(UserSchema, async ({ data }) => {
289
- return { user: await createUser(data) };
593
+ age: field.number()
594
+ .min(13, 'Must be 13+')
595
+ .minSoft(18, 'Parental consent required under 18')
596
+ .max(120, 'Invalid age')
597
+ .integer()
598
+ .required(),
599
+
600
+ acceptTerms: field.boolean()
601
+ .isTrue('You must accept the terms')
602
+ .required()
290
603
  });
291
604
  ```
292
605
 
293
- ### Frontend Adapters
606
+ ### E-Commerce Order
294
607
 
295
608
  ```typescript
296
- import { createForm, parseApiErrors, focusFirstError } from 'formschema/frontend';
609
+ const OrderSchema = schema({
610
+ customerId: field.string()
611
+ .alphanumeric()
612
+ .length(10)
613
+ .required(),
614
+
615
+ items: field.array(field.object(schema({
616
+ productId: field.string().required(),
617
+ quantity: field.number().min(1).integer(),
618
+ price: field.number().positive()
619
+ })))
620
+ .min(1, 'At least one item required')
621
+ .max(50, 'Maximum 50 items per order'),
622
+
623
+ total: field.number()
624
+ .positive()
625
+ .required(),
626
+
627
+ shippingAddress: field.object(schema({
628
+ street: field.string().required(),
629
+ city: field.string().required(),
630
+ state: field.string().uppercase().length(2),
631
+ zipCode: field.string().pattern(/^\d{5}$/)
632
+ })).required(),
633
+
634
+ shippingDate: field.date()
635
+ .future('Must be a future date')
636
+ .weekday('No weekend shipping')
637
+ });
638
+ ```
297
639
 
298
- const form = createForm(UserSchema, {
299
- initialValues: { email: '', password: '' },
300
- validateOnChange: true,
301
- validateOnBlur: true,
302
- onSubmit: async (values, helpers) => {
303
- // Submit logic
304
- },
640
+ ### API Configuration
641
+
642
+ ```typescript
643
+ const ServerConfigSchema = schema({
644
+ host: field.string()
645
+ .ipAddress('Invalid IP address')
646
+ .required(),
647
+
648
+ port: field.number()
649
+ .port('Invalid port number')
650
+ .required(),
651
+
652
+ ssl: field.boolean()
653
+ .required(),
654
+
655
+ maxConnections: field.number()
656
+ .integer()
657
+ .positive()
658
+ .between(1, 10000),
659
+
660
+ timeout: field.number()
661
+ .integer()
662
+ .positive()
663
+ .multipleOf(1000, 'Must be in seconds (1000ms)')
305
664
  });
665
+ ```
306
666
 
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();
667
+ ## 🚀 Migration Guide
314
668
 
315
- // Get field props for binding
316
- const props = form.getFieldProps('email');
317
- // { name, value, onChange, onBlur, error, hasError, warning, hasWarning, ... }
669
+ ### From Yup
318
670
 
319
- // Handle server errors
320
- const result = parseApiErrors(apiResponse);
321
- form.setServerErrors(result.hardErrors);
322
- ```
671
+ ```typescript
672
+ // Yup
673
+ const schema = yup.object({
674
+ email: yup.string().email().required(),
675
+ age: yup.number().min(18)
676
+ });
323
677
 
324
- ## Enterprise Response Format
678
+ // Unischema
679
+ const schema = schema({
680
+ email: field.string().email().required(),
681
+ age: field.number().min(18)
682
+ });
683
+ ```
325
684
 
326
- FormSchema uses a consistent error structure compatible with enterprise systems:
685
+ ### From Zod
327
686
 
328
687
  ```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
- }
688
+ // Zod
689
+ const schema = z.object({
690
+ email: z.string().email(),
691
+ age: z.number().min(18)
692
+ });
339
693
 
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
- }
694
+ // Unischema
695
+ const schema = schema({
696
+ email: field.string().email(),
697
+ age: field.number().min(18)
698
+ });
346
699
  ```
347
700
 
348
- ## Incremental Adoption
701
+ ## 🎨 Bundle Size
702
+
703
+ Unischema is optimized for tree-shaking:
704
+
705
+ - **Full library**: ~15KB min+gzip
706
+ - **Core only**: ~5KB min+gzip
707
+ - **Single validator**: ~2KB min+gzip
349
708
 
350
- FormSchema supports incremental adoption in existing systems:
709
+ Import only what you use for minimal bundle size.
710
+
711
+ ## 📖 API Reference
712
+
713
+ ### Core Functions
351
714
 
352
715
  ```typescript
353
- // Use only backend validation
354
- import { validateInput } from 'formschema/backend';
355
- const { valid, data, response } = validateInput(UserSchema, input);
716
+ import {
717
+ schema, // Create schema
718
+ field, // Field builders
719
+ validate, // Validate data
720
+ validateSchema, // Validate with schema
721
+ isValid, // Boolean validation
722
+ assertValid, // Throws if invalid
723
+ extend, // Extend schema
724
+ pick, // Pick fields
725
+ omit, // Omit fields
726
+ merge, // Merge schemas
727
+ partial, // Make all optional
728
+ type InferInput, // Input type
729
+ type InferOutput // Output type
730
+ } from 'unischema';
731
+ ```
356
732
 
357
- // Use only type generation
358
- import { type InferInput } from 'formschema';
359
- type User = InferInput<typeof UserSchema>;
733
+ ### Backend
360
734
 
361
- // Use only frontend validation
362
- import { validate } from 'formschema';
363
- const result = validate(UserSchema.definition, formData);
735
+ ```typescript
736
+ import {
737
+ validateBody, // Validate request body
738
+ validateQuery, // Validate query params
739
+ validateParams, // Validate route params
740
+ withValidation, // Wrapper with validation
741
+ createHandler // Serverless handler
742
+ } from 'unischema/backend';
364
743
  ```
365
744
 
366
- ## License
745
+ ### Frontend
746
+
747
+ ```typescript
748
+ import {
749
+ createForm, // Create form helper
750
+ parseApiErrors, // Parse server errors
751
+ focusFirstError // Focus first error field
752
+ } from 'unischema/frontend';
753
+ ```
754
+
755
+ ## 🤝 Contributing
756
+
757
+ Contributions are welcome! Please check out the [GitHub repository](https://github.com/Gaurav-pasi/unischema).
758
+
759
+ ## 📄 License
760
+
761
+ MIT © [Gaurav Pasi](https://github.com/Gaurav-pasi)
762
+
763
+ ## 🔗 Links
764
+
765
+ - [npm package](https://www.npmjs.com/package/unischema)
766
+ - [GitHub repository](https://github.com/Gaurav-pasi/unischema)
767
+ - [Issue tracker](https://github.com/Gaurav-pasi/unischema/issues)
768
+
769
+ ---
367
770
 
368
- MIT
771
+ **Made with ❤️ for developers who value type safety and code reusability**