@nolwenture/formik-jsonschema-form 1.4.4 → 2.0.1
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.
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ export function MyFormComponent() {
|
|
|
166
166
|
return (
|
|
167
167
|
<Formik
|
|
168
168
|
initialValues={createInitialValues(schema)}
|
|
169
|
-
validationSchema={createValidationSchema(schema)}
|
|
169
|
+
validationSchema={createValidationSchema({schema})}
|
|
170
170
|
onSubmit={handleSubmit}
|
|
171
171
|
>
|
|
172
172
|
<Form schema={schema} uiSchema={uiSchema}></Form>
|
|
@@ -175,6 +175,170 @@ export function MyFormComponent() {
|
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
### Validation
|
|
179
|
+
|
|
180
|
+
The `createValidationSchema` function generates a Yup validation schema from your JSON Schema. It supports:
|
|
181
|
+
|
|
182
|
+
#### Supported JSON Schema Keywords
|
|
183
|
+
|
|
184
|
+
**String validation:**
|
|
185
|
+
- `minLength` - Minimum string length
|
|
186
|
+
- `maxLength` - Maximum string length
|
|
187
|
+
- `pattern` - Regular expression pattern
|
|
188
|
+
|
|
189
|
+
**Number validation:**
|
|
190
|
+
- `minimum` - Minimum value (inclusive)
|
|
191
|
+
- `maximum` - Maximum value (inclusive)
|
|
192
|
+
- `exclusiveMinimum` - Minimum value (exclusive)
|
|
193
|
+
- `exclusiveMaximum` - Maximum value (exclusive)
|
|
194
|
+
- `multipleOf` - Value must be a multiple of this number
|
|
195
|
+
|
|
196
|
+
**Array validation:**
|
|
197
|
+
- `minItems` - Minimum number of items
|
|
198
|
+
- `maxItems` - Maximum number of items
|
|
199
|
+
- `uniqueItems` - All items must be unique
|
|
200
|
+
|
|
201
|
+
**Equality validation:**
|
|
202
|
+
- `const` - Value must equal the specified constant
|
|
203
|
+
- `enum` - Value must be one of the specified values
|
|
204
|
+
- `not: { const: ... }` - Value must NOT equal the specified constant
|
|
205
|
+
|
|
206
|
+
**Required fields:**
|
|
207
|
+
- Fields listed in the `required` array are validated as required
|
|
208
|
+
|
|
209
|
+
#### Function Signature
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
createValidationSchema(
|
|
213
|
+
{ schema, uiSchema }, // Schema and optional UI schema
|
|
214
|
+
{ // Optional options object
|
|
215
|
+
localize, // Localization function i18next t compatible
|
|
216
|
+
definitions, // Use these definitions instead of definitions in schema
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Custom Error Messages
|
|
222
|
+
|
|
223
|
+
You can customize error messages using `ui:validation` in the uiSchema:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
const uiSchema = {
|
|
227
|
+
username: {
|
|
228
|
+
"ui:validation": {
|
|
229
|
+
error: {
|
|
230
|
+
minLength: "Username must be at least 3 characters",
|
|
231
|
+
pattern: "Username can only contain letters and numbers",
|
|
232
|
+
required: "Please enter a username",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Localization
|
|
240
|
+
|
|
241
|
+
Pass a `localize` function compatible with i18next to get localized error messages:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { useTranslation } from 'react-i18next';
|
|
245
|
+
|
|
246
|
+
const { t } = useTranslation();
|
|
247
|
+
const validationSchema = createValidationSchema(
|
|
248
|
+
{ schema, uiSchema },
|
|
249
|
+
{ localize: t }
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The function will be called with keys like `validation.error.minLength`, `validation.error.required`, etc.
|
|
254
|
+
|
|
255
|
+
#### Error Message Priority
|
|
256
|
+
|
|
257
|
+
1. **Custom UI message** - `uiSchema["ui:validation"].error[keyword]`
|
|
258
|
+
2. **Localized message** - Result from `localize("validation.error.{keyword}", params)`
|
|
259
|
+
3. **Default message** - Built-in English messages
|
|
260
|
+
|
|
261
|
+
#### Example
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
const schema = {
|
|
265
|
+
type: "object",
|
|
266
|
+
required: ["email", "age"],
|
|
267
|
+
properties: {
|
|
268
|
+
email: {
|
|
269
|
+
type: "string",
|
|
270
|
+
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
|
271
|
+
},
|
|
272
|
+
age: {
|
|
273
|
+
type: "number",
|
|
274
|
+
minimum: 18,
|
|
275
|
+
maximum: 120,
|
|
276
|
+
},
|
|
277
|
+
tags: {
|
|
278
|
+
type: "array",
|
|
279
|
+
minItems: 1,
|
|
280
|
+
maxItems: 5,
|
|
281
|
+
uniqueItems: true,
|
|
282
|
+
items: { type: "string" },
|
|
283
|
+
},
|
|
284
|
+
status: {
|
|
285
|
+
type: "string",
|
|
286
|
+
const: "active",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const uiSchema = {
|
|
292
|
+
email: {
|
|
293
|
+
"ui:validation": {
|
|
294
|
+
error: {
|
|
295
|
+
pattern: "Please enter a valid email address",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const validationSchema = createValidationSchema({ schema, uiSchema });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Password Confirmation (Cross-Field Validation)
|
|
305
|
+
|
|
306
|
+
Use `const: { $data: "1/fieldName" }` to validate that one field equals another. The `1/` prefix uses JSON Pointer syntax to reference a sibling field (go up 1 level):
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
const schema = {
|
|
310
|
+
type: "object",
|
|
311
|
+
required: ["password", "confirmPassword"],
|
|
312
|
+
properties: {
|
|
313
|
+
password: {
|
|
314
|
+
type: "string",
|
|
315
|
+
title: "Password",
|
|
316
|
+
minLength: 8,
|
|
317
|
+
},
|
|
318
|
+
confirmPassword: {
|
|
319
|
+
type: "string",
|
|
320
|
+
title: "Confirm password",
|
|
321
|
+
const: { $data: "1/password" }, // Must match password field
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const uiSchema = {
|
|
327
|
+
confirmPassword: {
|
|
328
|
+
"ui:validation": {
|
|
329
|
+
error: {
|
|
330
|
+
const: "Passwords must match",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const validationSchema = createValidationSchema({ schema, uiSchema });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The `$data` reference syntax follows the AJV validation library pattern with JSON Pointer paths. This is only supported for `const` keyword at the moment, but it can be extended to support other keywords in the future (e.g. minimum to compare endDate is after startDate).
|
|
340
|
+
|
|
341
|
+
|
|
178
342
|
### Using custom components
|
|
179
343
|
|
|
180
344
|
Form components (such as wrappers) can be overridden with custom ones
|
|
@@ -37,6 +37,23 @@ exports.createValidationSchema = createValidationSchema;
|
|
|
37
37
|
const Yup = __importStar(require("yup"));
|
|
38
38
|
const resolveSchema_1 = require("./resolveSchema");
|
|
39
39
|
const solveReferences_1 = require("./solveReferences");
|
|
40
|
+
const DEFAULT_ERROR_MESSAGES = {
|
|
41
|
+
required: "Required",
|
|
42
|
+
minLength: (params) => `Must be at least ${params.limit} characters`,
|
|
43
|
+
maxLength: (params) => `Must be at most ${params.limit} characters`,
|
|
44
|
+
pattern: "Invalid format",
|
|
45
|
+
minimum: (params) => `Must be at least ${params.min}`,
|
|
46
|
+
maximum: (params) => `Must be at most ${params.max}`,
|
|
47
|
+
exclusiveMinimum: (params) => `Must be more than ${params.min}`,
|
|
48
|
+
exclusiveMaximum: (params) => `Must be less than ${params.max}`,
|
|
49
|
+
multipleOf: (params) => `Must be a multiple of ${params.multipleOf}`,
|
|
50
|
+
minItems: (params) => `Must have at least ${params.min} items`,
|
|
51
|
+
maxItems: (params) => `Must have at most ${params.max} items`,
|
|
52
|
+
uniqueItems: "Items must be unique",
|
|
53
|
+
enum: "Invalid value",
|
|
54
|
+
const: "Invalid value",
|
|
55
|
+
notConst: "Invalid value",
|
|
56
|
+
};
|
|
40
57
|
// Difficult to solve
|
|
41
58
|
// @ts-expect-error
|
|
42
59
|
Yup.addMethod(Yup.array, "mixedOf", function ({ itemSchemas, additionalItemSchema }) {
|
|
@@ -48,7 +65,7 @@ Yup.addMethod(Yup.array, "mixedOf", function ({ itemSchemas, additionalItemSchem
|
|
|
48
65
|
const numOfAdditionalItems = itemSchemas.length > valueLength ? 0 : valueLength - itemSchemas.length;
|
|
49
66
|
const additionalSchemas = additionalItemSchema
|
|
50
67
|
? Array(numOfAdditionalItems).fill(additionalItemSchema)
|
|
51
|
-
:
|
|
68
|
+
: Array(numOfAdditionalItems).fill(Yup.mixed());
|
|
52
69
|
return Yup.tuple([...itemSchemas, ...additionalSchemas]).typeError("Required");
|
|
53
70
|
});
|
|
54
71
|
});
|
|
@@ -156,22 +173,39 @@ function getYupType(schema) {
|
|
|
156
173
|
throw new Error(`createValidationSchema: Unsupported schema type: ${type}`);
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
|
-
function createValidationSchema(
|
|
176
|
+
function createValidationSchema(schemas, options) {
|
|
177
|
+
const { schema, uiSchema = {} } = schemas;
|
|
178
|
+
const { localize, definitions, conditionsContext = {}, condition } = options ?? {};
|
|
160
179
|
if (!schema?.properties) {
|
|
161
180
|
return Yup.object().shape({});
|
|
162
181
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
182
|
+
const getErrorMessage = (key, fieldUiSchema, params) => {
|
|
183
|
+
const uiError = fieldUiSchema?.["ui:validation"]?.error?.[key];
|
|
184
|
+
if (uiError)
|
|
185
|
+
return uiError;
|
|
186
|
+
if (localize) {
|
|
187
|
+
return localize(`validation.error.${key}`, params);
|
|
188
|
+
}
|
|
189
|
+
const defaultMsg = DEFAULT_ERROR_MESSAGES[key];
|
|
190
|
+
return typeof defaultMsg === "function" ? defaultMsg(params) : defaultMsg;
|
|
166
191
|
};
|
|
192
|
+
const schemaDefinitions = (definitions ?? schema.definitions ?? {});
|
|
167
193
|
const requiredFields = schema.required;
|
|
168
194
|
function getValue(givenSchema, key, required) {
|
|
169
195
|
const { schema } = (0, solveReferences_1.solveReferences)(givenSchema, schemaDefinitions, {}, {});
|
|
170
196
|
const isRequired = requiredFields?.includes(key) || required === true;
|
|
171
197
|
let type = schema.type;
|
|
172
198
|
let yupSchema;
|
|
199
|
+
const fieldUiSchema = uiSchema?.[key];
|
|
173
200
|
if (schema.properties) {
|
|
174
|
-
yupSchema = createValidationSchema(
|
|
201
|
+
yupSchema = createValidationSchema({
|
|
202
|
+
schema,
|
|
203
|
+
uiSchema: fieldUiSchema
|
|
204
|
+
}, {
|
|
205
|
+
localize,
|
|
206
|
+
definitions: schemaDefinitions,
|
|
207
|
+
conditionsContext,
|
|
208
|
+
});
|
|
175
209
|
type = type ?? "object";
|
|
176
210
|
}
|
|
177
211
|
else if (type === "array" && schema.anyOf) {
|
|
@@ -186,7 +220,9 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
|
|
|
186
220
|
return Yup.mixed().oneOf(anyOf.map((item) => item.const));
|
|
187
221
|
}));
|
|
188
222
|
// In case the property is required, there should be at least one item in the array
|
|
189
|
-
yupSchema = isRequired
|
|
223
|
+
yupSchema = isRequired
|
|
224
|
+
? anyOfSchema.min(1, getErrorMessage("required", fieldUiSchema))
|
|
225
|
+
: anyOfSchema;
|
|
190
226
|
}
|
|
191
227
|
}
|
|
192
228
|
else if (type === "array") {
|
|
@@ -218,8 +254,99 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
|
|
|
218
254
|
}
|
|
219
255
|
if (type === "null")
|
|
220
256
|
yupSchema.nullable();
|
|
257
|
+
// Apply JSON Schema validation keywords based on type
|
|
258
|
+
if (type === "string") {
|
|
259
|
+
let stringSchema = yupSchema;
|
|
260
|
+
if (schema.minLength !== undefined) {
|
|
261
|
+
stringSchema = stringSchema.min(schema.minLength, getErrorMessage("minLength", fieldUiSchema, { limit: schema.minLength }));
|
|
262
|
+
}
|
|
263
|
+
if (schema.maxLength !== undefined) {
|
|
264
|
+
stringSchema = stringSchema.max(schema.maxLength, getErrorMessage("maxLength", fieldUiSchema, { limit: schema.maxLength }));
|
|
265
|
+
}
|
|
266
|
+
if (schema.pattern !== undefined) {
|
|
267
|
+
stringSchema = stringSchema.matches(new RegExp(schema.pattern), getErrorMessage("pattern", fieldUiSchema));
|
|
268
|
+
}
|
|
269
|
+
yupSchema = stringSchema;
|
|
270
|
+
}
|
|
271
|
+
if (type === "number" || type === "integer") {
|
|
272
|
+
let numberSchema = yupSchema;
|
|
273
|
+
if (schema.minimum !== undefined) {
|
|
274
|
+
numberSchema = numberSchema.min(schema.minimum, getErrorMessage("minimum", fieldUiSchema, { min: schema.minimum }));
|
|
275
|
+
}
|
|
276
|
+
if (schema.maximum !== undefined) {
|
|
277
|
+
numberSchema = numberSchema.max(schema.maximum, getErrorMessage("maximum", fieldUiSchema, { max: schema.maximum }));
|
|
278
|
+
}
|
|
279
|
+
if (schema.exclusiveMinimum !== undefined) {
|
|
280
|
+
numberSchema = numberSchema.moreThan(schema.exclusiveMinimum, getErrorMessage("exclusiveMinimum", fieldUiSchema, { min: schema.exclusiveMinimum }));
|
|
281
|
+
}
|
|
282
|
+
if (schema.exclusiveMaximum !== undefined) {
|
|
283
|
+
numberSchema = numberSchema.lessThan(schema.exclusiveMaximum, getErrorMessage("exclusiveMaximum", fieldUiSchema, { max: schema.exclusiveMaximum }));
|
|
284
|
+
}
|
|
285
|
+
if (schema.multipleOf !== undefined) {
|
|
286
|
+
numberSchema = numberSchema.test("multipleOf", getErrorMessage("multipleOf", fieldUiSchema, { multipleOf: schema.multipleOf }), (value) => value === undefined ||
|
|
287
|
+
value === null ||
|
|
288
|
+
value % schema.multipleOf === 0);
|
|
289
|
+
}
|
|
290
|
+
yupSchema = numberSchema;
|
|
291
|
+
}
|
|
292
|
+
if (type === "array") {
|
|
293
|
+
const applyArrayTests = (s) => {
|
|
294
|
+
let result = s;
|
|
295
|
+
if (schema.minItems !== undefined) {
|
|
296
|
+
result = result.test("minItems", getErrorMessage("minItems", fieldUiSchema, { min: schema.minItems }), (value) => !value || value.length >= schema.minItems);
|
|
297
|
+
}
|
|
298
|
+
if (schema.maxItems !== undefined) {
|
|
299
|
+
result = result.test("maxItems", getErrorMessage("maxItems", fieldUiSchema, { max: schema.maxItems }), (value) => !value || value.length <= schema.maxItems);
|
|
300
|
+
}
|
|
301
|
+
if (schema.uniqueItems === true) {
|
|
302
|
+
result = result.test("uniqueItems", getErrorMessage("uniqueItems", fieldUiSchema), (value) => {
|
|
303
|
+
if (!value)
|
|
304
|
+
return true;
|
|
305
|
+
return new Set(value).size === value.length;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
};
|
|
310
|
+
if (yupSchema.test) {
|
|
311
|
+
yupSchema = applyArrayTests(yupSchema);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// It's likely a Lazy schema (from mixedOf). Wrap it to apply tests.
|
|
315
|
+
const originalLazy = yupSchema;
|
|
316
|
+
yupSchema = Yup.lazy((value, options) => {
|
|
317
|
+
return applyArrayTests(originalLazy.resolve(options));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const isOptionEnum = schema.enum !== undefined;
|
|
322
|
+
const isOptionConst = schema.const !== undefined;
|
|
323
|
+
if (isOptionEnum || isOptionConst) {
|
|
324
|
+
// Check for $data reference (cross-field validation)
|
|
325
|
+
const constValue = schema.const;
|
|
326
|
+
const isDataRef = typeof constValue === "object" && constValue?.$data;
|
|
327
|
+
if (isDataRef) {
|
|
328
|
+
// Cross-field reference: const: { $data: "1/otherFieldName" }
|
|
329
|
+
// JSON Pointer format: "1/fieldName" means go up 1 level and reference fieldName
|
|
330
|
+
const dataPath = constValue.$data;
|
|
331
|
+
const pathParts = dataPath.split("/");
|
|
332
|
+
const refFieldName = pathParts[pathParts.length - 1]; // Get the field name (last part)
|
|
333
|
+
yupSchema = yupSchema.oneOf([Yup.ref(refFieldName)], getErrorMessage("const", fieldUiSchema));
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Standard const or enum validation
|
|
337
|
+
const allowedValues = isOptionConst ? [schema.const] : schema.enum;
|
|
338
|
+
const errorKey = isOptionConst ? "const" : "enum";
|
|
339
|
+
yupSchema = yupSchema.oneOf(allowedValues, getErrorMessage(errorKey, fieldUiSchema));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (schema.not &&
|
|
343
|
+
typeof schema.not !== "boolean" &&
|
|
344
|
+
schema.not.const !== undefined) {
|
|
345
|
+
yupSchema = yupSchema.notOneOf([schema.not.const], getErrorMessage("notConst", fieldUiSchema));
|
|
346
|
+
}
|
|
347
|
+
// JSON Schema validation keywords
|
|
221
348
|
if (isRequired && type !== "array") {
|
|
222
|
-
yupSchema = yupSchema.required("
|
|
349
|
+
yupSchema = yupSchema.required(getErrorMessage("required", fieldUiSchema));
|
|
223
350
|
}
|
|
224
351
|
if (!condition) {
|
|
225
352
|
return yupSchema;
|
|
@@ -257,6 +384,14 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
|
|
|
257
384
|
override ones in the base, including type, presence and nullability."
|
|
258
385
|
Source: https://github.com/jquense/yup?tab=readme-ov-file#schemaconcatschema-schema-schema
|
|
259
386
|
*/
|
|
260
|
-
return validationSchema.concat(createValidationSchema(
|
|
387
|
+
return validationSchema.concat(createValidationSchema({
|
|
388
|
+
schema: solvedSchemas.schema,
|
|
389
|
+
uiSchema
|
|
390
|
+
}, {
|
|
391
|
+
localize,
|
|
392
|
+
definitions: schemaDefinitions,
|
|
393
|
+
conditionsContext,
|
|
394
|
+
condition: conditionalValidation.condition
|
|
395
|
+
}));
|
|
261
396
|
}, baseValidationSchema);
|
|
262
397
|
}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import * as Yup from "yup";
|
|
2
2
|
import { resolveOneOfValue } from "./resolveSchema";
|
|
3
3
|
import { solveReferences } from "./solveReferences";
|
|
4
|
+
const DEFAULT_ERROR_MESSAGES = {
|
|
5
|
+
required: "Required",
|
|
6
|
+
minLength: (params) => `Must be at least ${params.limit} characters`,
|
|
7
|
+
maxLength: (params) => `Must be at most ${params.limit} characters`,
|
|
8
|
+
pattern: "Invalid format",
|
|
9
|
+
minimum: (params) => `Must be at least ${params.min}`,
|
|
10
|
+
maximum: (params) => `Must be at most ${params.max}`,
|
|
11
|
+
exclusiveMinimum: (params) => `Must be more than ${params.min}`,
|
|
12
|
+
exclusiveMaximum: (params) => `Must be less than ${params.max}`,
|
|
13
|
+
multipleOf: (params) => `Must be a multiple of ${params.multipleOf}`,
|
|
14
|
+
minItems: (params) => `Must have at least ${params.min} items`,
|
|
15
|
+
maxItems: (params) => `Must have at most ${params.max} items`,
|
|
16
|
+
uniqueItems: "Items must be unique",
|
|
17
|
+
enum: "Invalid value",
|
|
18
|
+
const: "Invalid value",
|
|
19
|
+
notConst: "Invalid value",
|
|
20
|
+
};
|
|
4
21
|
// Difficult to solve
|
|
5
22
|
// @ts-expect-error
|
|
6
23
|
Yup.addMethod(Yup.array, "mixedOf", function ({ itemSchemas, additionalItemSchema }) {
|
|
@@ -12,7 +29,7 @@ Yup.addMethod(Yup.array, "mixedOf", function ({ itemSchemas, additionalItemSchem
|
|
|
12
29
|
const numOfAdditionalItems = itemSchemas.length > valueLength ? 0 : valueLength - itemSchemas.length;
|
|
13
30
|
const additionalSchemas = additionalItemSchema
|
|
14
31
|
? Array(numOfAdditionalItems).fill(additionalItemSchema)
|
|
15
|
-
:
|
|
32
|
+
: Array(numOfAdditionalItems).fill(Yup.mixed());
|
|
16
33
|
return Yup.tuple([...itemSchemas, ...additionalSchemas]).typeError("Required");
|
|
17
34
|
});
|
|
18
35
|
});
|
|
@@ -120,22 +137,39 @@ function getYupType(schema) {
|
|
|
120
137
|
throw new Error(`createValidationSchema: Unsupported schema type: ${type}`);
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
|
-
export function createValidationSchema(
|
|
140
|
+
export function createValidationSchema(schemas, options) {
|
|
141
|
+
const { schema, uiSchema = {} } = schemas;
|
|
142
|
+
const { localize, definitions, conditionsContext = {}, condition } = options ?? {};
|
|
124
143
|
if (!schema?.properties) {
|
|
125
144
|
return Yup.object().shape({});
|
|
126
145
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
const getErrorMessage = (key, fieldUiSchema, params) => {
|
|
147
|
+
const uiError = fieldUiSchema?.["ui:validation"]?.error?.[key];
|
|
148
|
+
if (uiError)
|
|
149
|
+
return uiError;
|
|
150
|
+
if (localize) {
|
|
151
|
+
return localize(`validation.error.${key}`, params);
|
|
152
|
+
}
|
|
153
|
+
const defaultMsg = DEFAULT_ERROR_MESSAGES[key];
|
|
154
|
+
return typeof defaultMsg === "function" ? defaultMsg(params) : defaultMsg;
|
|
130
155
|
};
|
|
156
|
+
const schemaDefinitions = (definitions ?? schema.definitions ?? {});
|
|
131
157
|
const requiredFields = schema.required;
|
|
132
158
|
function getValue(givenSchema, key, required) {
|
|
133
159
|
const { schema } = solveReferences(givenSchema, schemaDefinitions, {}, {});
|
|
134
160
|
const isRequired = requiredFields?.includes(key) || required === true;
|
|
135
161
|
let type = schema.type;
|
|
136
162
|
let yupSchema;
|
|
163
|
+
const fieldUiSchema = uiSchema?.[key];
|
|
137
164
|
if (schema.properties) {
|
|
138
|
-
yupSchema = createValidationSchema(
|
|
165
|
+
yupSchema = createValidationSchema({
|
|
166
|
+
schema,
|
|
167
|
+
uiSchema: fieldUiSchema
|
|
168
|
+
}, {
|
|
169
|
+
localize,
|
|
170
|
+
definitions: schemaDefinitions,
|
|
171
|
+
conditionsContext,
|
|
172
|
+
});
|
|
139
173
|
type = type ?? "object";
|
|
140
174
|
}
|
|
141
175
|
else if (type === "array" && schema.anyOf) {
|
|
@@ -150,7 +184,9 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
|
|
|
150
184
|
return Yup.mixed().oneOf(anyOf.map((item) => item.const));
|
|
151
185
|
}));
|
|
152
186
|
// In case the property is required, there should be at least one item in the array
|
|
153
|
-
yupSchema = isRequired
|
|
187
|
+
yupSchema = isRequired
|
|
188
|
+
? anyOfSchema.min(1, getErrorMessage("required", fieldUiSchema))
|
|
189
|
+
: anyOfSchema;
|
|
154
190
|
}
|
|
155
191
|
}
|
|
156
192
|
else if (type === "array") {
|
|
@@ -182,8 +218,99 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
|
|
|
182
218
|
}
|
|
183
219
|
if (type === "null")
|
|
184
220
|
yupSchema.nullable();
|
|
221
|
+
// Apply JSON Schema validation keywords based on type
|
|
222
|
+
if (type === "string") {
|
|
223
|
+
let stringSchema = yupSchema;
|
|
224
|
+
if (schema.minLength !== undefined) {
|
|
225
|
+
stringSchema = stringSchema.min(schema.minLength, getErrorMessage("minLength", fieldUiSchema, { limit: schema.minLength }));
|
|
226
|
+
}
|
|
227
|
+
if (schema.maxLength !== undefined) {
|
|
228
|
+
stringSchema = stringSchema.max(schema.maxLength, getErrorMessage("maxLength", fieldUiSchema, { limit: schema.maxLength }));
|
|
229
|
+
}
|
|
230
|
+
if (schema.pattern !== undefined) {
|
|
231
|
+
stringSchema = stringSchema.matches(new RegExp(schema.pattern), getErrorMessage("pattern", fieldUiSchema));
|
|
232
|
+
}
|
|
233
|
+
yupSchema = stringSchema;
|
|
234
|
+
}
|
|
235
|
+
if (type === "number" || type === "integer") {
|
|
236
|
+
let numberSchema = yupSchema;
|
|
237
|
+
if (schema.minimum !== undefined) {
|
|
238
|
+
numberSchema = numberSchema.min(schema.minimum, getErrorMessage("minimum", fieldUiSchema, { min: schema.minimum }));
|
|
239
|
+
}
|
|
240
|
+
if (schema.maximum !== undefined) {
|
|
241
|
+
numberSchema = numberSchema.max(schema.maximum, getErrorMessage("maximum", fieldUiSchema, { max: schema.maximum }));
|
|
242
|
+
}
|
|
243
|
+
if (schema.exclusiveMinimum !== undefined) {
|
|
244
|
+
numberSchema = numberSchema.moreThan(schema.exclusiveMinimum, getErrorMessage("exclusiveMinimum", fieldUiSchema, { min: schema.exclusiveMinimum }));
|
|
245
|
+
}
|
|
246
|
+
if (schema.exclusiveMaximum !== undefined) {
|
|
247
|
+
numberSchema = numberSchema.lessThan(schema.exclusiveMaximum, getErrorMessage("exclusiveMaximum", fieldUiSchema, { max: schema.exclusiveMaximum }));
|
|
248
|
+
}
|
|
249
|
+
if (schema.multipleOf !== undefined) {
|
|
250
|
+
numberSchema = numberSchema.test("multipleOf", getErrorMessage("multipleOf", fieldUiSchema, { multipleOf: schema.multipleOf }), (value) => value === undefined ||
|
|
251
|
+
value === null ||
|
|
252
|
+
value % schema.multipleOf === 0);
|
|
253
|
+
}
|
|
254
|
+
yupSchema = numberSchema;
|
|
255
|
+
}
|
|
256
|
+
if (type === "array") {
|
|
257
|
+
const applyArrayTests = (s) => {
|
|
258
|
+
let result = s;
|
|
259
|
+
if (schema.minItems !== undefined) {
|
|
260
|
+
result = result.test("minItems", getErrorMessage("minItems", fieldUiSchema, { min: schema.minItems }), (value) => !value || value.length >= schema.minItems);
|
|
261
|
+
}
|
|
262
|
+
if (schema.maxItems !== undefined) {
|
|
263
|
+
result = result.test("maxItems", getErrorMessage("maxItems", fieldUiSchema, { max: schema.maxItems }), (value) => !value || value.length <= schema.maxItems);
|
|
264
|
+
}
|
|
265
|
+
if (schema.uniqueItems === true) {
|
|
266
|
+
result = result.test("uniqueItems", getErrorMessage("uniqueItems", fieldUiSchema), (value) => {
|
|
267
|
+
if (!value)
|
|
268
|
+
return true;
|
|
269
|
+
return new Set(value).size === value.length;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
};
|
|
274
|
+
if (yupSchema.test) {
|
|
275
|
+
yupSchema = applyArrayTests(yupSchema);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// It's likely a Lazy schema (from mixedOf). Wrap it to apply tests.
|
|
279
|
+
const originalLazy = yupSchema;
|
|
280
|
+
yupSchema = Yup.lazy((value, options) => {
|
|
281
|
+
return applyArrayTests(originalLazy.resolve(options));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const isOptionEnum = schema.enum !== undefined;
|
|
286
|
+
const isOptionConst = schema.const !== undefined;
|
|
287
|
+
if (isOptionEnum || isOptionConst) {
|
|
288
|
+
// Check for $data reference (cross-field validation)
|
|
289
|
+
const constValue = schema.const;
|
|
290
|
+
const isDataRef = typeof constValue === "object" && constValue?.$data;
|
|
291
|
+
if (isDataRef) {
|
|
292
|
+
// Cross-field reference: const: { $data: "1/otherFieldName" }
|
|
293
|
+
// JSON Pointer format: "1/fieldName" means go up 1 level and reference fieldName
|
|
294
|
+
const dataPath = constValue.$data;
|
|
295
|
+
const pathParts = dataPath.split("/");
|
|
296
|
+
const refFieldName = pathParts[pathParts.length - 1]; // Get the field name (last part)
|
|
297
|
+
yupSchema = yupSchema.oneOf([Yup.ref(refFieldName)], getErrorMessage("const", fieldUiSchema));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// Standard const or enum validation
|
|
301
|
+
const allowedValues = isOptionConst ? [schema.const] : schema.enum;
|
|
302
|
+
const errorKey = isOptionConst ? "const" : "enum";
|
|
303
|
+
yupSchema = yupSchema.oneOf(allowedValues, getErrorMessage(errorKey, fieldUiSchema));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (schema.not &&
|
|
307
|
+
typeof schema.not !== "boolean" &&
|
|
308
|
+
schema.not.const !== undefined) {
|
|
309
|
+
yupSchema = yupSchema.notOneOf([schema.not.const], getErrorMessage("notConst", fieldUiSchema));
|
|
310
|
+
}
|
|
311
|
+
// JSON Schema validation keywords
|
|
185
312
|
if (isRequired && type !== "array") {
|
|
186
|
-
yupSchema = yupSchema.required("
|
|
313
|
+
yupSchema = yupSchema.required(getErrorMessage("required", fieldUiSchema));
|
|
187
314
|
}
|
|
188
315
|
if (!condition) {
|
|
189
316
|
return yupSchema;
|
|
@@ -221,6 +348,14 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
|
|
|
221
348
|
override ones in the base, including type, presence and nullability."
|
|
222
349
|
Source: https://github.com/jquense/yup?tab=readme-ov-file#schemaconcatschema-schema-schema
|
|
223
350
|
*/
|
|
224
|
-
return validationSchema.concat(createValidationSchema(
|
|
351
|
+
return validationSchema.concat(createValidationSchema({
|
|
352
|
+
schema: solvedSchemas.schema,
|
|
353
|
+
uiSchema
|
|
354
|
+
}, {
|
|
355
|
+
localize,
|
|
356
|
+
definitions: schemaDefinitions,
|
|
357
|
+
conditionsContext,
|
|
358
|
+
condition: conditionalValidation.condition
|
|
359
|
+
}));
|
|
225
360
|
}, baseValidationSchema);
|
|
226
361
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Yup from "yup";
|
|
2
2
|
import { Schema } from "../common/types";
|
|
3
|
+
import { UiSchema } from "../common/types";
|
|
4
|
+
export type LocalizeType = (key: string, params?: Record<string, any>) => string;
|
|
3
5
|
type Condition = {
|
|
4
6
|
dependentKeys: string[];
|
|
5
7
|
conditionFn: (...value: any[]) => boolean;
|
|
@@ -17,5 +19,13 @@ type ConditionsContext = {
|
|
|
17
19
|
condition: Condition;
|
|
18
20
|
yupSchema: Yup.Schema<any, any, any, "">;
|
|
19
21
|
};
|
|
20
|
-
export declare function createValidationSchema(
|
|
22
|
+
export declare function createValidationSchema(schemas: {
|
|
23
|
+
schema: Schema;
|
|
24
|
+
uiSchema?: UiSchema;
|
|
25
|
+
}, options?: {
|
|
26
|
+
localize?: LocalizeType;
|
|
27
|
+
definitions?: Schema;
|
|
28
|
+
conditionsContext?: Record<string, ConditionsContext[]>;
|
|
29
|
+
condition?: Condition;
|
|
30
|
+
}): Yup.ObjectSchema<{}, Yup.AnyObject, {}, "">;
|
|
21
31
|
export {};
|