@forgehive/schema 0.1.4 → 0.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.
@@ -0,0 +1,191 @@
1
+ import { Schema, type SchemaDescription } from '../index'
2
+
3
+ // `Schema.from()` rebuilds a Schema from standard JSON Schema. Each test feeds
4
+ // an EXPLICIT JSON Schema object and then validates concrete data, so the
5
+ // accepted input format and its effect are obvious at a glance.
6
+
7
+ describe('Schema.from(JSON Schema) -> validation', () => {
8
+ it('basic types', () => {
9
+ const jsonSchema: SchemaDescription = {
10
+ type: 'object',
11
+ properties: {
12
+ name: { type: 'string' },
13
+ age: { type: 'number' },
14
+ active: { type: 'boolean' },
15
+ },
16
+ required: ['name', 'age', 'active'],
17
+ }
18
+
19
+ const schema = Schema.from(jsonSchema)
20
+
21
+ expect(schema.validate({ name: 'John', age: 30, active: true })).toBe(true)
22
+ expect(schema.validate({ name: 'John', age: '30', active: true })).toBe(false) // age not a number
23
+ expect(schema.validate({ name: 'John', age: 30 })).toBe(false) // active missing
24
+ })
25
+
26
+ it('string validations (min, max, pattern)', () => {
27
+ const jsonSchema: SchemaDescription = {
28
+ type: 'object',
29
+ properties: {
30
+ username: {
31
+ type: 'string',
32
+ minLength: 3,
33
+ maxLength: 20,
34
+ pattern: '^[a-zA-Z0-9_]+$',
35
+ },
36
+ },
37
+ required: ['username'],
38
+ }
39
+
40
+ const schema = Schema.from(jsonSchema)
41
+
42
+ expect(schema.validate({ username: 'john_doe' })).toBe(true)
43
+ expect(schema.validate({ username: 'ab' })).toBe(false) // too short
44
+ expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false) // too long
45
+ expect(schema.validate({ username: 'john-doe' })).toBe(false) // pattern mismatch
46
+ })
47
+
48
+ it('number validations (minimum, maximum)', () => {
49
+ const jsonSchema: SchemaDescription = {
50
+ type: 'object',
51
+ properties: {
52
+ age: { type: 'number', minimum: 18, maximum: 100 },
53
+ },
54
+ required: ['age'],
55
+ }
56
+
57
+ const schema = Schema.from(jsonSchema)
58
+
59
+ expect(schema.validate({ age: 25 })).toBe(true)
60
+ expect(schema.validate({ age: 18 })).toBe(true)
61
+ expect(schema.validate({ age: 17 })).toBe(false) // below minimum
62
+ expect(schema.validate({ age: 101 })).toBe(false) // above maximum
63
+ })
64
+
65
+ it('optional fields (absent from `required`)', () => {
66
+ const jsonSchema: SchemaDescription = {
67
+ type: 'object',
68
+ properties: {
69
+ name: { type: 'string' },
70
+ age: { type: 'number' },
71
+ },
72
+ required: ['name'], // age is optional
73
+ }
74
+
75
+ const schema = Schema.from(jsonSchema)
76
+
77
+ expect(schema.validate({ name: 'John', age: 30 })).toBe(true)
78
+ expect(schema.validate({ name: 'John' })).toBe(true) // optional age omitted
79
+ expect(schema.validate({ age: 30 })).toBe(false) // required name omitted
80
+ })
81
+
82
+ it('arrays', () => {
83
+ const jsonSchema: SchemaDescription = {
84
+ type: 'object',
85
+ properties: {
86
+ tags: { type: 'array', items: { type: 'string' } },
87
+ },
88
+ required: ['tags'],
89
+ }
90
+
91
+ const schema = Schema.from(jsonSchema)
92
+
93
+ expect(schema.validate({ tags: ['a', 'b'] })).toBe(true)
94
+ expect(schema.validate({ tags: [] })).toBe(true)
95
+ expect(schema.validate({ tags: ['a', 2] })).toBe(false) // wrong item type
96
+ })
97
+
98
+ it('records (additionalProperties)', () => {
99
+ const jsonSchema: SchemaDescription = {
100
+ type: 'object',
101
+ properties: {
102
+ data: {
103
+ type: 'object',
104
+ propertyNames: { type: 'string' },
105
+ additionalProperties: { type: 'number' },
106
+ },
107
+ },
108
+ required: ['data'],
109
+ }
110
+
111
+ const schema = Schema.from(jsonSchema)
112
+
113
+ expect(schema.validate({ data: { a: 1, b: 2 } })).toBe(true)
114
+ expect(schema.validate({ data: { a: 'x' } })).toBe(false) // value not a number
115
+ })
116
+
117
+ it('mixed records (anyOf additionalProperties)', () => {
118
+ const jsonSchema: SchemaDescription = {
119
+ type: 'object',
120
+ properties: {
121
+ data: {
122
+ type: 'object',
123
+ propertyNames: { type: 'string' },
124
+ additionalProperties: {
125
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
126
+ },
127
+ },
128
+ },
129
+ required: ['data'],
130
+ }
131
+
132
+ const schema = Schema.from(jsonSchema)
133
+
134
+ expect(schema.validate({ data: { a: 'x', b: 1, c: true } })).toBe(true)
135
+ expect(schema.validate({ data: { a: [1, 2] } })).toBe(false) // array not allowed
136
+ })
137
+
138
+ it('nested objects', () => {
139
+ const jsonSchema: SchemaDescription = {
140
+ type: 'object',
141
+ properties: {
142
+ user: {
143
+ type: 'object',
144
+ properties: {
145
+ name: { type: 'string', minLength: 2 },
146
+ age: { type: 'number' },
147
+ },
148
+ required: ['name'],
149
+ },
150
+ },
151
+ required: ['user'],
152
+ }
153
+
154
+ const schema = Schema.from(jsonSchema)
155
+
156
+ expect(schema.validate({ user: { name: 'John', age: 30 } })).toBe(true)
157
+ expect(schema.validate({ user: { name: 'John' } })).toBe(true) // nested age optional
158
+ expect(schema.validate({ user: { name: 'J' } })).toBe(false) // name too short
159
+ expect(schema.validate({ user: { age: 30 } })).toBe(false) // nested name required
160
+ })
161
+
162
+ it('string format: email', () => {
163
+ const jsonSchema: SchemaDescription = {
164
+ type: 'object',
165
+ properties: {
166
+ email: { type: 'string', format: 'email' },
167
+ },
168
+ required: ['email'],
169
+ }
170
+
171
+ const schema = Schema.from(jsonSchema)
172
+
173
+ expect(schema.validate({ email: 'john@example.com' })).toBe(true)
174
+ expect(schema.validate({ email: 'not-an-email' })).toBe(false)
175
+ })
176
+
177
+ it('string format: date-time', () => {
178
+ const jsonSchema: SchemaDescription = {
179
+ type: 'object',
180
+ properties: {
181
+ createdAt: { type: 'string', format: 'date-time' },
182
+ },
183
+ required: ['createdAt'],
184
+ }
185
+
186
+ const schema = Schema.from(jsonSchema)
187
+
188
+ expect(schema.validate({ createdAt: '2024-03-20T12:00:00Z' })).toBe(true)
189
+ expect(schema.validate({ createdAt: 'not-a-date' })).toBe(false)
190
+ })
191
+ })
@@ -1,4 +1,4 @@
1
- import Schema, { type SchemaDescription } from '../index'
1
+ import Schema from '../index'
2
2
 
3
3
  describe('Schema basic types', () => {
4
4
  it('should validate a string', () => {
@@ -28,16 +28,18 @@ describe('Schema basic types', () => {
28
28
  })
29
29
  })
30
30
 
31
- describe('Schema description', () => {
31
+ describe('Schema description (JSON Schema)', () => {
32
32
  it('should describe a string', () => {
33
33
  const schema = new Schema({
34
34
  name: Schema.string(),
35
35
  })
36
36
 
37
37
  const result = schema.describe()
38
- expect(result).toEqual({
38
+ expect(result.type).toBe('object')
39
+ expect(result.properties).toEqual({
39
40
  name: { type: 'string' },
40
41
  })
42
+ expect(result.required).toEqual(['name'])
41
43
  })
42
44
 
43
45
  it('should describe a string and number', () => {
@@ -47,26 +49,29 @@ describe('Schema description', () => {
47
49
  })
48
50
 
49
51
  const result = schema.describe()
50
- expect(result).toEqual({
52
+ expect(result.properties).toEqual({
51
53
  name: { type: 'string' },
52
54
  age: { type: 'number' },
53
55
  })
56
+ expect(result.required).toEqual(['name', 'age'])
54
57
  })
55
58
  })
56
59
 
57
60
  describe('Schema hydrate', () => {
58
- it('should describe a string', () => {
59
- const description: SchemaDescription = {
60
- name: { type: 'string' },
61
- age: { type: 'number' },
62
- }
63
- const schema = Schema.from(description)
61
+ it('should rebuild a schema from its description', () => {
62
+ const original = new Schema({
63
+ name: Schema.string(),
64
+ age: Schema.number(),
65
+ })
66
+
67
+ const schema = Schema.from(original.describe())
64
68
 
65
69
  const result = schema.describe()
66
- expect(result).toEqual({
70
+ expect(result.properties).toEqual({
67
71
  name: { type: 'string' },
68
72
  age: { type: 'number' },
69
73
  })
74
+ expect(result.required).toEqual(['name', 'age'])
70
75
  })
71
76
  })
72
77
 
@@ -96,20 +101,31 @@ describe('Schema validation errors', () => {
96
101
  })
97
102
  })
98
103
 
99
- // describe('Schema optional fields', () => {
100
- // it('should validate with missing optional field', () => {
101
- // const schema = new Schema({
102
- // name: Schema.string(),
103
- // age: Schema.number().optional(),
104
- // })
104
+ describe('Schema optional fields', () => {
105
+ it('should validate with missing optional field', () => {
106
+ const schema = new Schema({
107
+ name: Schema.string(),
108
+ age: Schema.number().optional(),
109
+ })
110
+
111
+ const result = schema.validate({
112
+ name: 'World',
113
+ })
114
+
115
+ expect(result).toBe(true)
116
+ })
105
117
 
106
- // const result = schema.validate({
107
- // name: 'World',
108
- // })
118
+ it('should omit optional fields from the required list', () => {
119
+ const schema = new Schema({
120
+ name: Schema.string(),
121
+ age: Schema.number().optional(),
122
+ })
109
123
 
110
- // expect(result).toBe(true)
111
- // })
112
- // })
124
+ const result = schema.describe()
125
+ expect(result.required).toEqual(['name'])
126
+ expect(result.properties).toHaveProperty('age')
127
+ })
128
+ })
113
129
 
114
130
  describe('Schema arrays', () => {
115
131
  it('should validate array of strings', () => {
@@ -167,32 +183,102 @@ describe('Schema arrays', () => {
167
183
  })
168
184
 
169
185
  const result = schema.describe()
170
- expect(result).toEqual({
186
+ expect(result.properties).toEqual({
171
187
  tags: { type: 'array', items: { type: 'string' } },
172
188
  scores: { type: 'array', items: { type: 'number' } },
173
189
  })
174
190
  })
175
191
 
176
192
  it('should hydrate array schema from description', () => {
177
- const description: SchemaDescription = {
178
- tags: { type: 'array', items: { type: 'string' } },
179
- scores: { type: 'array', items: { type: 'number' } },
180
- }
181
- const schema = Schema.from(description)
193
+ const original = new Schema({
194
+ tags: Schema.array(Schema.string()),
195
+ scores: Schema.array(Schema.number()),
196
+ })
197
+ const schema = Schema.from(original.describe())
182
198
 
183
199
  const result = schema.describe()
184
- expect(result).toEqual({
200
+ expect(result.properties).toEqual({
185
201
  tags: { type: 'array', items: { type: 'string' } },
186
202
  scores: { type: 'array', items: { type: 'number' } },
187
203
  })
188
204
  })
189
205
  })
190
206
 
207
+ describe('Schema nested objects', () => {
208
+ it('should validate and describe nested object fields', () => {
209
+ const schema = new Schema({
210
+ user: Schema.object({
211
+ name: Schema.string(),
212
+ age: Schema.number().optional(),
213
+ }),
214
+ })
215
+
216
+ expect(schema.validate({ user: { name: 'John', age: 30 } })).toBe(true)
217
+ expect(schema.validate({ user: { name: 'John' } })).toBe(true)
218
+ expect(schema.validate({ user: { age: 30 } })).toBe(false)
219
+
220
+ const result = schema.describe()
221
+ expect(result.properties?.user).toMatchObject({
222
+ type: 'object',
223
+ properties: { name: { type: 'string' }, age: { type: 'number' } },
224
+ required: ['name'],
225
+ })
226
+ })
227
+
228
+ it('should round-trip a nested object schema', () => {
229
+ const original = new Schema({
230
+ user: Schema.object({
231
+ name: Schema.string().min(3),
232
+ }),
233
+ })
234
+
235
+ const clone = Schema.from(original.describe())
236
+ expect(clone.validate({ user: { name: 'John' } })).toBe(true)
237
+ expect(clone.validate({ user: { name: 'Jo' } })).toBe(false)
238
+ })
239
+
240
+ it('should infer types through safeParse for an optional nested object', () => {
241
+ const base = new Schema({
242
+ name: Schema.string().describe('The name of the user'),
243
+ address: Schema.object({
244
+ street: Schema.string(),
245
+ city: Schema.string(),
246
+ state: Schema.string(),
247
+ zip: Schema.string(),
248
+ }).optional().describe('The address of the user'),
249
+ })
250
+
251
+ // Optional nested object omitted
252
+ const withoutAddress = base.safeParse({ name: 'World' })
253
+ expect(withoutAddress.success).toBe(true)
254
+ if (withoutAddress.success) {
255
+ // res.data.name is string, res.data.address is optional
256
+ const name: string = withoutAddress.data.name
257
+ const street: string = withoutAddress.data.address?.street ?? ''
258
+ expect(name).toBe('World')
259
+ expect(street).toBe('')
260
+ }
261
+
262
+ // Optional nested object present
263
+ const withAddress = base.safeParse({
264
+ name: 'World',
265
+ address: { street: 'Main', city: 'Town', state: 'CA', zip: '00000' },
266
+ })
267
+ expect(withAddress.success).toBe(true)
268
+ if (withAddress.success) {
269
+ expect(withAddress.data.address?.street).toBe('Main')
270
+ }
271
+
272
+ // Partial nested object is rejected (street/city/... required when present)
273
+ expect(base.validate({ name: 'World', address: { street: 'Main' } })).toBe(false)
274
+ })
275
+ })
276
+
191
277
  describe('Schema custom validation', () => {
192
278
  it('should validate with custom rules', () => {
193
279
  const schema = new Schema({
194
280
  age: Schema.number().min(0).max(120),
195
- email: Schema.string().email(),
281
+ email: Schema.email(),
196
282
  })
197
283
 
198
284
  const result = schema.validate({
@@ -202,4 +288,35 @@ describe('Schema custom validation', () => {
202
288
 
203
289
  expect(result).toBe(true)
204
290
  })
291
+
292
+ it('should serialize element descriptions', () => {
293
+ const schema = new Schema({
294
+ name: Schema.string().describe('the user name'),
295
+ })
296
+
297
+ const result = schema.describe()
298
+ expect(result.properties?.name).toMatchObject({
299
+ type: 'string',
300
+ description: 'the user name',
301
+ })
302
+ })
303
+
304
+ it('should keep element descriptions through the describe/from round-trip', () => {
305
+ const schema = new Schema({
306
+ name: Schema.string().describe('The name of the user'),
307
+ age: Schema.number().describe('The age of the user'),
308
+ })
309
+
310
+ const clone = Schema.from(schema.describe())
311
+ const result = clone.describe()
312
+
313
+ expect(result.properties?.name).toMatchObject({
314
+ type: 'string',
315
+ description: 'The name of the user',
316
+ })
317
+ expect(result.properties?.age).toMatchObject({
318
+ type: 'number',
319
+ description: 'The age of the user',
320
+ })
321
+ })
205
322
  })
@@ -1,11 +1,11 @@
1
1
  import { Schema } from '../index'
2
2
 
3
- describe('Schema Custom Validations', () => {
3
+ describe('Schema Optional Fields', () => {
4
4
  describe('Optional Fields', () => {
5
5
  it('should handle optional string fields', () => {
6
- const schema = Schema.from({
7
- name: { type: 'string', optional: true },
8
- age: { type: 'number' }
6
+ const schema = new Schema({
7
+ name: Schema.string().optional(),
8
+ age: Schema.number()
9
9
  })
10
10
 
11
11
  // Valid with optional field present
@@ -19,9 +19,9 @@ describe('Schema Custom Validations', () => {
19
19
  })
20
20
 
21
21
  it('should handle optional array fields', () => {
22
- const schema = Schema.from({
23
- tags: { type: 'array', items: { type: 'string' }, optional: true },
24
- scores: { type: 'array', items: { type: 'number' } }
22
+ const schema = new Schema({
23
+ tags: Schema.array(Schema.string()).optional(),
24
+ scores: Schema.array(Schema.number())
25
25
  })
26
26
 
27
27
  // Valid with optional array present
@@ -35,26 +35,28 @@ describe('Schema Custom Validations', () => {
35
35
  })
36
36
 
37
37
  it('should correctly describe optional fields', () => {
38
- const schema = Schema.from({
39
- name: { type: 'string', optional: true },
40
- age: { type: 'number' },
41
- tags: { type: 'array', items: { type: 'string' }, optional: true }
38
+ const schema = new Schema({
39
+ name: Schema.string().optional(),
40
+ age: Schema.number(),
41
+ tags: Schema.array(Schema.string()).optional()
42
42
  })
43
43
 
44
44
  const description = schema.describe()
45
45
 
46
- expect(description.name).toEqual({ type: 'string', optional: true })
47
- expect(description.age).toEqual({ type: 'number' })
48
- expect(description.tags).toEqual({ type: 'array', items: { type: 'string' }, optional: true })
46
+ // Optionality is expressed by absence from the `required` list
47
+ expect(description.required).toEqual(['age'])
48
+ expect(description.properties?.name).toEqual({ type: 'string' })
49
+ expect(description.properties?.age).toEqual({ type: 'number' })
50
+ expect(description.properties?.tags).toEqual({ type: 'array', items: { type: 'string' } })
49
51
  })
50
52
  })
51
53
 
52
54
  describe('Schema Roundtrip', () => {
53
55
  it('should maintain optional fields through describe/from cycle', () => {
54
- const originalSchema = Schema.from({
55
- name: { type: 'string', optional: true },
56
- age: { type: 'number' },
57
- tags: { type: 'array', items: { type: 'string' }, optional: true }
56
+ const originalSchema = new Schema({
57
+ name: Schema.string().optional(),
58
+ age: Schema.number(),
59
+ tags: Schema.array(Schema.string()).optional()
58
60
  })
59
61
 
60
62
  const description = originalSchema.describe()
@@ -68,6 +70,9 @@ describe('Schema Custom Validations', () => {
68
70
  expect(reconstructedSchema.validate(validData)).toBe(true)
69
71
  expect(originalSchema.validate(dataWithoutOptional)).toBe(true)
70
72
  expect(reconstructedSchema.validate(dataWithoutOptional)).toBe(true)
73
+
74
+ // The reconstructed schema preserves which field stayed required
75
+ expect(reconstructedSchema.describe().required).toEqual(['age'])
71
76
  })
72
77
  })
73
78
  })
@@ -0,0 +1,160 @@
1
+ import { Schema } from '../index'
2
+
3
+ describe('Schema.parse()', () => {
4
+ it('returns typed data on success', () => {
5
+ const schema = new Schema({
6
+ name: Schema.string(),
7
+ age: Schema.number(),
8
+ })
9
+
10
+ const data = schema.parse({ name: 'John', age: 30 })
11
+
12
+ expect(data).toEqual({ name: 'John', age: 30 })
13
+ // Type-level: the result is { name: string; age: number }
14
+ const name: string = data.name
15
+ const age: number = data.age
16
+ expect(name).toBe('John')
17
+ expect(age).toBe(30)
18
+ })
19
+
20
+ it('strips unknown keys', () => {
21
+ const schema = new Schema({
22
+ name: Schema.string(),
23
+ })
24
+
25
+ const data = schema.parse({ name: 'John', extra: 'ignored' })
26
+
27
+ expect(data).toEqual({ name: 'John' })
28
+ expect(data).not.toHaveProperty('extra')
29
+ })
30
+
31
+ it('coerces nothing and throws ZodError on invalid data', () => {
32
+ const schema = new Schema({
33
+ name: Schema.string(),
34
+ age: Schema.number(),
35
+ })
36
+
37
+ expect(() => schema.parse({ name: 'John', age: 'thirty' })).toThrow()
38
+ })
39
+
40
+ it('throws a ZodError exposing issues with path and message', () => {
41
+ const schema = new Schema({
42
+ name: Schema.string(),
43
+ age: Schema.number().min(18),
44
+ })
45
+
46
+ try {
47
+ schema.parse({ name: 123, age: 10 })
48
+ throw new Error('expected parse to throw')
49
+ } catch (err) {
50
+ const zodError = err as { issues?: Array<{ path: PropertyKey[]; message: string }> }
51
+ expect(Array.isArray(zodError.issues)).toBe(true)
52
+ const paths = (zodError.issues ?? []).map((issue) => issue.path.join('.'))
53
+ expect(paths).toContain('name')
54
+ expect(paths).toContain('age')
55
+ }
56
+ })
57
+
58
+ it('parses nested objects and optionals', () => {
59
+ const schema = new Schema({
60
+ user: Schema.object({
61
+ name: Schema.string(),
62
+ nickname: Schema.string().optional(),
63
+ }),
64
+ })
65
+
66
+ expect(schema.parse({ user: { name: 'John' } })).toEqual({ user: { name: 'John' } })
67
+ expect(schema.parse({ user: { name: 'John', nickname: 'JJ' } })).toEqual({
68
+ user: { name: 'John', nickname: 'JJ' },
69
+ })
70
+ expect(() => schema.parse({ user: {} })).toThrow()
71
+ })
72
+ })
73
+
74
+ describe('Schema.safeParse()', () => {
75
+ it('returns success: true with data on valid input', () => {
76
+ const schema = new Schema({
77
+ name: Schema.string(),
78
+ age: Schema.number(),
79
+ })
80
+
81
+ const result = schema.safeParse({ name: 'John', age: 30 })
82
+
83
+ expect(result.success).toBe(true)
84
+ if (result.success) {
85
+ expect(result.data).toEqual({ name: 'John', age: 30 })
86
+ // Type-level: result.data is fully typed
87
+ const name: string = result.data.name
88
+ expect(name).toBe('John')
89
+ }
90
+ })
91
+
92
+ it('returns success: false with an error on invalid input', () => {
93
+ const schema = new Schema({
94
+ name: Schema.string(),
95
+ age: Schema.number(),
96
+ })
97
+
98
+ const result = schema.safeParse({ name: 'John', age: 'thirty' })
99
+
100
+ expect(result.success).toBe(false)
101
+ if (!result.success) {
102
+ expect(result.error).toBeDefined()
103
+ expect(Array.isArray(result.error.issues)).toBe(true)
104
+ const paths = result.error.issues.map((issue) => issue.path.join('.'))
105
+ expect(paths).toContain('age')
106
+ }
107
+ })
108
+
109
+ it('does not throw on invalid input', () => {
110
+ const schema = new Schema({
111
+ age: Schema.number(),
112
+ })
113
+
114
+ expect(() => schema.safeParse({ age: 'nope' })).not.toThrow()
115
+ })
116
+
117
+ it('reports each failing field in the issues list', () => {
118
+ const schema = new Schema({
119
+ email: Schema.email(),
120
+ age: Schema.number().min(18),
121
+ username: Schema.string().min(3),
122
+ })
123
+
124
+ const result = schema.safeParse({ email: 'nope', age: 5, username: 'ab' })
125
+
126
+ expect(result.success).toBe(false)
127
+ if (!result.success) {
128
+ const paths = result.error.issues.map((issue) => issue.path.join('.'))
129
+ expect(paths).toEqual(expect.arrayContaining(['email', 'age', 'username']))
130
+ }
131
+ })
132
+
133
+ it('strips unknown keys on success', () => {
134
+ const schema = new Schema({
135
+ name: Schema.string(),
136
+ })
137
+
138
+ const result = schema.safeParse({ name: 'John', extra: true })
139
+
140
+ expect(result.success).toBe(true)
141
+ if (result.success) {
142
+ expect(result.data).toEqual({ name: 'John' })
143
+ }
144
+ })
145
+
146
+ it('infers optional fields as possibly undefined', () => {
147
+ const schema = new Schema({
148
+ name: Schema.string(),
149
+ age: Schema.number().optional(),
150
+ })
151
+
152
+ const result = schema.safeParse({ name: 'John' })
153
+
154
+ expect(result.success).toBe(true)
155
+ if (result.success) {
156
+ const age: number | undefined = result.data.age
157
+ expect(age).toBeUndefined()
158
+ }
159
+ })
160
+ })