@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,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("../index");
4
+ // `describe()` serializes a Schema to standard JSON Schema (draft 2020-12).
5
+ // Each test builds a Schema with the Schema.* helpers and asserts the EXACT
6
+ // JSON Schema it produces, so the wire format is obvious at a glance.
7
+ const $schema = 'https://json-schema.org/draft/2020-12/schema';
8
+ describe('Schema.describe() -> JSON Schema', () => {
9
+ it('basic types', () => {
10
+ const schema = new index_1.Schema({
11
+ name: index_1.Schema.string(),
12
+ age: index_1.Schema.number(),
13
+ active: index_1.Schema.boolean(),
14
+ });
15
+ expect(schema.describe()).toEqual({
16
+ $schema,
17
+ type: 'object',
18
+ properties: {
19
+ name: { type: 'string' },
20
+ age: { type: 'number' },
21
+ active: { type: 'boolean' },
22
+ },
23
+ required: ['name', 'age', 'active'],
24
+ additionalProperties: false,
25
+ });
26
+ });
27
+ it('string validations', () => {
28
+ const schema = new index_1.Schema({
29
+ username: index_1.Schema.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
30
+ });
31
+ expect(schema.describe()).toEqual({
32
+ $schema,
33
+ type: 'object',
34
+ properties: {
35
+ username: {
36
+ type: 'string',
37
+ minLength: 3,
38
+ maxLength: 20,
39
+ pattern: '^[a-zA-Z0-9_]+$',
40
+ },
41
+ },
42
+ required: ['username'],
43
+ additionalProperties: false,
44
+ });
45
+ });
46
+ it('number validations', () => {
47
+ const schema = new index_1.Schema({
48
+ age: index_1.Schema.number().min(18).max(100),
49
+ });
50
+ expect(schema.describe()).toEqual({
51
+ $schema,
52
+ type: 'object',
53
+ properties: {
54
+ age: { type: 'number', minimum: 18, maximum: 100 },
55
+ },
56
+ required: ['age'],
57
+ additionalProperties: false,
58
+ });
59
+ });
60
+ it('optional fields are omitted from `required`', () => {
61
+ const schema = new index_1.Schema({
62
+ name: index_1.Schema.string(),
63
+ age: index_1.Schema.number().optional(),
64
+ });
65
+ expect(schema.describe()).toEqual({
66
+ $schema,
67
+ type: 'object',
68
+ properties: {
69
+ name: { type: 'string' },
70
+ age: { type: 'number' },
71
+ },
72
+ required: ['name'], // age is optional, so it is not required
73
+ additionalProperties: false,
74
+ });
75
+ });
76
+ it('arrays', () => {
77
+ const schema = new index_1.Schema({
78
+ tags: index_1.Schema.array(index_1.Schema.string()),
79
+ scores: index_1.Schema.array(index_1.Schema.number()),
80
+ });
81
+ expect(schema.describe()).toEqual({
82
+ $schema,
83
+ type: 'object',
84
+ properties: {
85
+ tags: { type: 'array', items: { type: 'string' } },
86
+ scores: { type: 'array', items: { type: 'number' } },
87
+ },
88
+ required: ['tags', 'scores'],
89
+ additionalProperties: false,
90
+ });
91
+ });
92
+ it('records', () => {
93
+ const schema = new index_1.Schema({
94
+ strings: index_1.Schema.stringRecord(),
95
+ mixed: index_1.Schema.mixedRecord(),
96
+ });
97
+ expect(schema.describe()).toEqual({
98
+ $schema,
99
+ type: 'object',
100
+ properties: {
101
+ strings: {
102
+ type: 'object',
103
+ propertyNames: { type: 'string' },
104
+ additionalProperties: { type: 'string' },
105
+ },
106
+ mixed: {
107
+ type: 'object',
108
+ propertyNames: { type: 'string' },
109
+ additionalProperties: {
110
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
111
+ },
112
+ },
113
+ },
114
+ required: ['strings', 'mixed'],
115
+ additionalProperties: false,
116
+ });
117
+ });
118
+ it('nested objects', () => {
119
+ const schema = new index_1.Schema({
120
+ user: index_1.Schema.object({
121
+ name: index_1.Schema.string(),
122
+ age: index_1.Schema.number().optional(),
123
+ }),
124
+ });
125
+ expect(schema.describe()).toEqual({
126
+ $schema,
127
+ type: 'object',
128
+ properties: {
129
+ user: {
130
+ type: 'object',
131
+ properties: {
132
+ name: { type: 'string' },
133
+ age: { type: 'number' },
134
+ },
135
+ required: ['name'],
136
+ additionalProperties: false,
137
+ },
138
+ },
139
+ required: ['user'],
140
+ additionalProperties: false,
141
+ });
142
+ });
143
+ it('element descriptions', () => {
144
+ const schema = new index_1.Schema({
145
+ name: index_1.Schema.string().describe('The name of the user'),
146
+ });
147
+ expect(schema.describe()).toEqual({
148
+ $schema,
149
+ type: 'object',
150
+ properties: {
151
+ name: { type: 'string', description: 'The name of the user' },
152
+ },
153
+ required: ['name'],
154
+ additionalProperties: false,
155
+ });
156
+ });
157
+ it('string formats: email, uuid, url, date', () => {
158
+ const schema = new index_1.Schema({
159
+ email: index_1.Schema.email(),
160
+ id: index_1.Schema.uuid(),
161
+ site: index_1.Schema.url(),
162
+ when: index_1.Schema.date(),
163
+ });
164
+ // Format keywords are the stable part; the generated `pattern` strings are
165
+ // long and version-specific, so we assert the meaningful keys.
166
+ const properties = schema.describe().properties;
167
+ expect(properties?.email).toMatchObject({ type: 'string', format: 'email' });
168
+ expect(properties?.id).toMatchObject({ type: 'string', format: 'uuid' });
169
+ expect(properties?.site).toMatchObject({ type: 'string', format: 'uri' });
170
+ expect(properties?.when).toMatchObject({ type: 'string', format: 'date-time' });
171
+ });
172
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("../index");
4
+ // `Schema.from()` rebuilds a Schema from standard JSON Schema. Each test feeds
5
+ // an EXPLICIT JSON Schema object and then validates concrete data, so the
6
+ // accepted input format and its effect are obvious at a glance.
7
+ describe('Schema.from(JSON Schema) -> validation', () => {
8
+ it('basic types', () => {
9
+ const jsonSchema = {
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
+ const schema = index_1.Schema.from(jsonSchema);
19
+ expect(schema.validate({ name: 'John', age: 30, active: true })).toBe(true);
20
+ expect(schema.validate({ name: 'John', age: '30', active: true })).toBe(false); // age not a number
21
+ expect(schema.validate({ name: 'John', age: 30 })).toBe(false); // active missing
22
+ });
23
+ it('string validations (min, max, pattern)', () => {
24
+ const jsonSchema = {
25
+ type: 'object',
26
+ properties: {
27
+ username: {
28
+ type: 'string',
29
+ minLength: 3,
30
+ maxLength: 20,
31
+ pattern: '^[a-zA-Z0-9_]+$',
32
+ },
33
+ },
34
+ required: ['username'],
35
+ };
36
+ const schema = index_1.Schema.from(jsonSchema);
37
+ expect(schema.validate({ username: 'john_doe' })).toBe(true);
38
+ expect(schema.validate({ username: 'ab' })).toBe(false); // too short
39
+ expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false); // too long
40
+ expect(schema.validate({ username: 'john-doe' })).toBe(false); // pattern mismatch
41
+ });
42
+ it('number validations (minimum, maximum)', () => {
43
+ const jsonSchema = {
44
+ type: 'object',
45
+ properties: {
46
+ age: { type: 'number', minimum: 18, maximum: 100 },
47
+ },
48
+ required: ['age'],
49
+ };
50
+ const schema = index_1.Schema.from(jsonSchema);
51
+ expect(schema.validate({ age: 25 })).toBe(true);
52
+ expect(schema.validate({ age: 18 })).toBe(true);
53
+ expect(schema.validate({ age: 17 })).toBe(false); // below minimum
54
+ expect(schema.validate({ age: 101 })).toBe(false); // above maximum
55
+ });
56
+ it('optional fields (absent from `required`)', () => {
57
+ const jsonSchema = {
58
+ type: 'object',
59
+ properties: {
60
+ name: { type: 'string' },
61
+ age: { type: 'number' },
62
+ },
63
+ required: ['name'], // age is optional
64
+ };
65
+ const schema = index_1.Schema.from(jsonSchema);
66
+ expect(schema.validate({ name: 'John', age: 30 })).toBe(true);
67
+ expect(schema.validate({ name: 'John' })).toBe(true); // optional age omitted
68
+ expect(schema.validate({ age: 30 })).toBe(false); // required name omitted
69
+ });
70
+ it('arrays', () => {
71
+ const jsonSchema = {
72
+ type: 'object',
73
+ properties: {
74
+ tags: { type: 'array', items: { type: 'string' } },
75
+ },
76
+ required: ['tags'],
77
+ };
78
+ const schema = index_1.Schema.from(jsonSchema);
79
+ expect(schema.validate({ tags: ['a', 'b'] })).toBe(true);
80
+ expect(schema.validate({ tags: [] })).toBe(true);
81
+ expect(schema.validate({ tags: ['a', 2] })).toBe(false); // wrong item type
82
+ });
83
+ it('records (additionalProperties)', () => {
84
+ const jsonSchema = {
85
+ type: 'object',
86
+ properties: {
87
+ data: {
88
+ type: 'object',
89
+ propertyNames: { type: 'string' },
90
+ additionalProperties: { type: 'number' },
91
+ },
92
+ },
93
+ required: ['data'],
94
+ };
95
+ const schema = index_1.Schema.from(jsonSchema);
96
+ expect(schema.validate({ data: { a: 1, b: 2 } })).toBe(true);
97
+ expect(schema.validate({ data: { a: 'x' } })).toBe(false); // value not a number
98
+ });
99
+ it('mixed records (anyOf additionalProperties)', () => {
100
+ const jsonSchema = {
101
+ type: 'object',
102
+ properties: {
103
+ data: {
104
+ type: 'object',
105
+ propertyNames: { type: 'string' },
106
+ additionalProperties: {
107
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
108
+ },
109
+ },
110
+ },
111
+ required: ['data'],
112
+ };
113
+ const schema = index_1.Schema.from(jsonSchema);
114
+ expect(schema.validate({ data: { a: 'x', b: 1, c: true } })).toBe(true);
115
+ expect(schema.validate({ data: { a: [1, 2] } })).toBe(false); // array not allowed
116
+ });
117
+ it('nested objects', () => {
118
+ const jsonSchema = {
119
+ type: 'object',
120
+ properties: {
121
+ user: {
122
+ type: 'object',
123
+ properties: {
124
+ name: { type: 'string', minLength: 2 },
125
+ age: { type: 'number' },
126
+ },
127
+ required: ['name'],
128
+ },
129
+ },
130
+ required: ['user'],
131
+ };
132
+ const schema = index_1.Schema.from(jsonSchema);
133
+ expect(schema.validate({ user: { name: 'John', age: 30 } })).toBe(true);
134
+ expect(schema.validate({ user: { name: 'John' } })).toBe(true); // nested age optional
135
+ expect(schema.validate({ user: { name: 'J' } })).toBe(false); // name too short
136
+ expect(schema.validate({ user: { age: 30 } })).toBe(false); // nested name required
137
+ });
138
+ it('string format: email', () => {
139
+ const jsonSchema = {
140
+ type: 'object',
141
+ properties: {
142
+ email: { type: 'string', format: 'email' },
143
+ },
144
+ required: ['email'],
145
+ };
146
+ const schema = index_1.Schema.from(jsonSchema);
147
+ expect(schema.validate({ email: 'john@example.com' })).toBe(true);
148
+ expect(schema.validate({ email: 'not-an-email' })).toBe(false);
149
+ });
150
+ it('string format: date-time', () => {
151
+ const jsonSchema = {
152
+ type: 'object',
153
+ properties: {
154
+ createdAt: { type: 'string', format: 'date-time' },
155
+ },
156
+ required: ['createdAt'],
157
+ };
158
+ const schema = index_1.Schema.from(jsonSchema);
159
+ expect(schema.validate({ createdAt: '2024-03-20T12:00:00Z' })).toBe(true);
160
+ expect(schema.validate({ createdAt: 'not-a-date' })).toBe(false);
161
+ });
162
+ });
@@ -26,15 +26,17 @@ describe('Schema basic types', () => {
26
26
  expect(result).toEqual(true);
27
27
  });
28
28
  });
29
- describe('Schema description', () => {
29
+ describe('Schema description (JSON Schema)', () => {
30
30
  it('should describe a string', () => {
31
31
  const schema = new index_1.default({
32
32
  name: index_1.default.string(),
33
33
  });
34
34
  const result = schema.describe();
35
- expect(result).toEqual({
35
+ expect(result.type).toBe('object');
36
+ expect(result.properties).toEqual({
36
37
  name: { type: 'string' },
37
38
  });
39
+ expect(result.required).toEqual(['name']);
38
40
  });
39
41
  it('should describe a string and number', () => {
40
42
  const schema = new index_1.default({
@@ -42,24 +44,26 @@ describe('Schema description', () => {
42
44
  age: index_1.default.number(),
43
45
  });
44
46
  const result = schema.describe();
45
- expect(result).toEqual({
47
+ expect(result.properties).toEqual({
46
48
  name: { type: 'string' },
47
49
  age: { type: 'number' },
48
50
  });
51
+ expect(result.required).toEqual(['name', 'age']);
49
52
  });
50
53
  });
51
54
  describe('Schema hydrate', () => {
52
- it('should describe a string', () => {
53
- const description = {
54
- name: { type: 'string' },
55
- age: { type: 'number' },
56
- };
57
- const schema = index_1.default.from(description);
55
+ it('should rebuild a schema from its description', () => {
56
+ const original = new index_1.default({
57
+ name: index_1.default.string(),
58
+ age: index_1.default.number(),
59
+ });
60
+ const schema = index_1.default.from(original.describe());
58
61
  const result = schema.describe();
59
- expect(result).toEqual({
62
+ expect(result.properties).toEqual({
60
63
  name: { type: 'string' },
61
64
  age: { type: 'number' },
62
65
  });
66
+ expect(result.required).toEqual(['name', 'age']);
63
67
  });
64
68
  });
65
69
  describe('Schema validation errors', () => {
@@ -82,18 +86,27 @@ describe('Schema validation errors', () => {
82
86
  expect(result).toBe(false);
83
87
  });
84
88
  });
85
- // describe('Schema optional fields', () => {
86
- // it('should validate with missing optional field', () => {
87
- // const schema = new Schema({
88
- // name: Schema.string(),
89
- // age: Schema.number().optional(),
90
- // })
91
- // const result = schema.validate({
92
- // name: 'World',
93
- // })
94
- // expect(result).toBe(true)
95
- // })
96
- // })
89
+ describe('Schema optional fields', () => {
90
+ it('should validate with missing optional field', () => {
91
+ const schema = new index_1.default({
92
+ name: index_1.default.string(),
93
+ age: index_1.default.number().optional(),
94
+ });
95
+ const result = schema.validate({
96
+ name: 'World',
97
+ });
98
+ expect(result).toBe(true);
99
+ });
100
+ it('should omit optional fields from the required list', () => {
101
+ const schema = new index_1.default({
102
+ name: index_1.default.string(),
103
+ age: index_1.default.number().optional(),
104
+ });
105
+ const result = schema.describe();
106
+ expect(result.required).toEqual(['name']);
107
+ expect(result.properties).toHaveProperty('age');
108
+ });
109
+ });
97
110
  describe('Schema arrays', () => {
98
111
  it('should validate array of strings', () => {
99
112
  const schema = new index_1.default({
@@ -137,29 +150,90 @@ describe('Schema arrays', () => {
137
150
  scores: index_1.default.array(index_1.default.number()),
138
151
  });
139
152
  const result = schema.describe();
140
- expect(result).toEqual({
153
+ expect(result.properties).toEqual({
141
154
  tags: { type: 'array', items: { type: 'string' } },
142
155
  scores: { type: 'array', items: { type: 'number' } },
143
156
  });
144
157
  });
145
158
  it('should hydrate array schema from description', () => {
146
- const description = {
147
- tags: { type: 'array', items: { type: 'string' } },
148
- scores: { type: 'array', items: { type: 'number' } },
149
- };
150
- const schema = index_1.default.from(description);
159
+ const original = new index_1.default({
160
+ tags: index_1.default.array(index_1.default.string()),
161
+ scores: index_1.default.array(index_1.default.number()),
162
+ });
163
+ const schema = index_1.default.from(original.describe());
151
164
  const result = schema.describe();
152
- expect(result).toEqual({
165
+ expect(result.properties).toEqual({
153
166
  tags: { type: 'array', items: { type: 'string' } },
154
167
  scores: { type: 'array', items: { type: 'number' } },
155
168
  });
156
169
  });
157
170
  });
171
+ describe('Schema nested objects', () => {
172
+ it('should validate and describe nested object fields', () => {
173
+ const schema = new index_1.default({
174
+ user: index_1.default.object({
175
+ name: index_1.default.string(),
176
+ age: index_1.default.number().optional(),
177
+ }),
178
+ });
179
+ expect(schema.validate({ user: { name: 'John', age: 30 } })).toBe(true);
180
+ expect(schema.validate({ user: { name: 'John' } })).toBe(true);
181
+ expect(schema.validate({ user: { age: 30 } })).toBe(false);
182
+ const result = schema.describe();
183
+ expect(result.properties?.user).toMatchObject({
184
+ type: 'object',
185
+ properties: { name: { type: 'string' }, age: { type: 'number' } },
186
+ required: ['name'],
187
+ });
188
+ });
189
+ it('should round-trip a nested object schema', () => {
190
+ const original = new index_1.default({
191
+ user: index_1.default.object({
192
+ name: index_1.default.string().min(3),
193
+ }),
194
+ });
195
+ const clone = index_1.default.from(original.describe());
196
+ expect(clone.validate({ user: { name: 'John' } })).toBe(true);
197
+ expect(clone.validate({ user: { name: 'Jo' } })).toBe(false);
198
+ });
199
+ it('should infer types through safeParse for an optional nested object', () => {
200
+ const base = new index_1.default({
201
+ name: index_1.default.string().describe('The name of the user'),
202
+ address: index_1.default.object({
203
+ street: index_1.default.string(),
204
+ city: index_1.default.string(),
205
+ state: index_1.default.string(),
206
+ zip: index_1.default.string(),
207
+ }).optional().describe('The address of the user'),
208
+ });
209
+ // Optional nested object omitted
210
+ const withoutAddress = base.safeParse({ name: 'World' });
211
+ expect(withoutAddress.success).toBe(true);
212
+ if (withoutAddress.success) {
213
+ // res.data.name is string, res.data.address is optional
214
+ const name = withoutAddress.data.name;
215
+ const street = withoutAddress.data.address?.street ?? '';
216
+ expect(name).toBe('World');
217
+ expect(street).toBe('');
218
+ }
219
+ // Optional nested object present
220
+ const withAddress = base.safeParse({
221
+ name: 'World',
222
+ address: { street: 'Main', city: 'Town', state: 'CA', zip: '00000' },
223
+ });
224
+ expect(withAddress.success).toBe(true);
225
+ if (withAddress.success) {
226
+ expect(withAddress.data.address?.street).toBe('Main');
227
+ }
228
+ // Partial nested object is rejected (street/city/... required when present)
229
+ expect(base.validate({ name: 'World', address: { street: 'Main' } })).toBe(false);
230
+ });
231
+ });
158
232
  describe('Schema custom validation', () => {
159
233
  it('should validate with custom rules', () => {
160
234
  const schema = new index_1.default({
161
235
  age: index_1.default.number().min(0).max(120),
162
- email: index_1.default.string().email(),
236
+ email: index_1.default.email(),
163
237
  });
164
238
  const result = schema.validate({
165
239
  age: 25,
@@ -167,4 +241,30 @@ describe('Schema custom validation', () => {
167
241
  });
168
242
  expect(result).toBe(true);
169
243
  });
244
+ it('should serialize element descriptions', () => {
245
+ const schema = new index_1.default({
246
+ name: index_1.default.string().describe('the user name'),
247
+ });
248
+ const result = schema.describe();
249
+ expect(result.properties?.name).toMatchObject({
250
+ type: 'string',
251
+ description: 'the user name',
252
+ });
253
+ });
254
+ it('should keep element descriptions through the describe/from round-trip', () => {
255
+ const schema = new index_1.default({
256
+ name: index_1.default.string().describe('The name of the user'),
257
+ age: index_1.default.number().describe('The age of the user'),
258
+ });
259
+ const clone = index_1.default.from(schema.describe());
260
+ const result = clone.describe();
261
+ expect(result.properties?.name).toMatchObject({
262
+ type: 'string',
263
+ description: 'The name of the user',
264
+ });
265
+ expect(result.properties?.age).toMatchObject({
266
+ type: 'number',
267
+ description: 'The age of the user',
268
+ });
269
+ });
170
270
  });
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const index_1 = require("../index");
4
- describe('Schema Custom Validations', () => {
4
+ describe('Schema Optional Fields', () => {
5
5
  describe('Optional Fields', () => {
6
6
  it('should handle optional string fields', () => {
7
- const schema = index_1.Schema.from({
8
- name: { type: 'string', optional: true },
9
- age: { type: 'number' }
7
+ const schema = new index_1.Schema({
8
+ name: index_1.Schema.string().optional(),
9
+ age: index_1.Schema.number()
10
10
  });
11
11
  // Valid with optional field present
12
12
  expect(schema.validate({ name: 'John', age: 30 })).toBe(true);
@@ -16,9 +16,9 @@ describe('Schema Custom Validations', () => {
16
16
  expect(schema.validate({ name: 'John' })).toBe(false);
17
17
  });
18
18
  it('should handle optional array fields', () => {
19
- const schema = index_1.Schema.from({
20
- tags: { type: 'array', items: { type: 'string' }, optional: true },
21
- scores: { type: 'array', items: { type: 'number' } }
19
+ const schema = new index_1.Schema({
20
+ tags: index_1.Schema.array(index_1.Schema.string()).optional(),
21
+ scores: index_1.Schema.array(index_1.Schema.number())
22
22
  });
23
23
  // Valid with optional array present
24
24
  expect(schema.validate({ tags: ['tag1', 'tag2'], scores: [1, 2, 3] })).toBe(true);
@@ -28,23 +28,25 @@ describe('Schema Custom Validations', () => {
28
28
  expect(schema.validate({ tags: ['tag1'] })).toBe(false);
29
29
  });
30
30
  it('should correctly describe optional fields', () => {
31
- const schema = index_1.Schema.from({
32
- name: { type: 'string', optional: true },
33
- age: { type: 'number' },
34
- tags: { type: 'array', items: { type: 'string' }, optional: true }
31
+ const schema = new index_1.Schema({
32
+ name: index_1.Schema.string().optional(),
33
+ age: index_1.Schema.number(),
34
+ tags: index_1.Schema.array(index_1.Schema.string()).optional()
35
35
  });
36
36
  const description = schema.describe();
37
- expect(description.name).toEqual({ type: 'string', optional: true });
38
- expect(description.age).toEqual({ type: 'number' });
39
- expect(description.tags).toEqual({ type: 'array', items: { type: 'string' }, optional: true });
37
+ // Optionality is expressed by absence from the `required` list
38
+ expect(description.required).toEqual(['age']);
39
+ expect(description.properties?.name).toEqual({ type: 'string' });
40
+ expect(description.properties?.age).toEqual({ type: 'number' });
41
+ expect(description.properties?.tags).toEqual({ type: 'array', items: { type: 'string' } });
40
42
  });
41
43
  });
42
44
  describe('Schema Roundtrip', () => {
43
45
  it('should maintain optional fields through describe/from cycle', () => {
44
- const originalSchema = index_1.Schema.from({
45
- name: { type: 'string', optional: true },
46
- age: { type: 'number' },
47
- tags: { type: 'array', items: { type: 'string' }, optional: true }
46
+ const originalSchema = new index_1.Schema({
47
+ name: index_1.Schema.string().optional(),
48
+ age: index_1.Schema.number(),
49
+ tags: index_1.Schema.array(index_1.Schema.string()).optional()
48
50
  });
49
51
  const description = originalSchema.describe();
50
52
  const reconstructedSchema = index_1.Schema.from(description);
@@ -55,6 +57,8 @@ describe('Schema Custom Validations', () => {
55
57
  expect(reconstructedSchema.validate(validData)).toBe(true);
56
58
  expect(originalSchema.validate(dataWithoutOptional)).toBe(true);
57
59
  expect(reconstructedSchema.validate(dataWithoutOptional)).toBe(true);
60
+ // The reconstructed schema preserves which field stayed required
61
+ expect(reconstructedSchema.describe().required).toEqual(['age']);
58
62
  });
59
63
  });
60
64
  });
@@ -0,0 +1 @@
1
+ export {};