@nolwenture/formik-jsonschema-form 1.4.3 → 2.0.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.
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,169 @@ 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
+ definitions, // Optional external definitions
215
+ { localize } // Optional localization function
216
+ )
217
+ ```
218
+
219
+ #### Custom Error Messages
220
+
221
+ You can customize error messages using `ui:validation` in the uiSchema:
222
+
223
+ ```tsx
224
+ const uiSchema = {
225
+ username: {
226
+ "ui:validation": {
227
+ error: {
228
+ minLength: "Username must be at least 3 characters",
229
+ pattern: "Username can only contain letters and numbers",
230
+ required: "Please enter a username",
231
+ },
232
+ },
233
+ },
234
+ };
235
+ ```
236
+
237
+ #### Localization
238
+
239
+ Pass a `localize` function compatible with i18next to get localized error messages:
240
+
241
+ ```tsx
242
+ import { useTranslation } from 'react-i18next';
243
+
244
+ const { t } = useTranslation();
245
+ const validationSchema = createValidationSchema(
246
+ { schema, uiSchema },
247
+ {},
248
+ { localize: t }
249
+ );
250
+ ```
251
+
252
+ The function will be called with keys like `validation.error.minLength`, `validation.error.required`, etc.
253
+
254
+ #### Error Message Priority
255
+
256
+ 1. **Custom UI message** - `uiSchema["ui:validation"].error[keyword]`
257
+ 2. **Localized message** - Result from `localize("validation.error.{keyword}", params)`
258
+ 3. **Default message** - Built-in English messages
259
+
260
+ #### Example
261
+
262
+ ```tsx
263
+ const schema = {
264
+ type: "object",
265
+ required: ["email", "age"],
266
+ properties: {
267
+ email: {
268
+ type: "string",
269
+ pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
270
+ },
271
+ age: {
272
+ type: "number",
273
+ minimum: 18,
274
+ maximum: 120,
275
+ },
276
+ tags: {
277
+ type: "array",
278
+ minItems: 1,
279
+ maxItems: 5,
280
+ uniqueItems: true,
281
+ items: { type: "string" },
282
+ },
283
+ status: {
284
+ type: "string",
285
+ const: "active",
286
+ },
287
+ },
288
+ };
289
+
290
+ const uiSchema = {
291
+ email: {
292
+ "ui:validation": {
293
+ error: {
294
+ pattern: "Please enter a valid email address",
295
+ },
296
+ },
297
+ },
298
+ };
299
+
300
+ const validationSchema = createValidationSchema({ schema, uiSchema });
301
+ ```
302
+
303
+ #### Password Confirmation (Cross-Field Validation)
304
+
305
+ 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):
306
+
307
+ ```tsx
308
+ const schema = {
309
+ type: "object",
310
+ required: ["password", "confirmPassword"],
311
+ properties: {
312
+ password: {
313
+ type: "string",
314
+ title: "Password",
315
+ minLength: 8,
316
+ },
317
+ confirmPassword: {
318
+ type: "string",
319
+ title: "Confirm password",
320
+ const: { $data: "1/password" }, // Must match password field
321
+ },
322
+ },
323
+ };
324
+
325
+ const uiSchema = {
326
+ confirmPassword: {
327
+ "ui:validation": {
328
+ error: {
329
+ const: "Passwords must match",
330
+ },
331
+ },
332
+ },
333
+ };
334
+
335
+ const validationSchema = createValidationSchema({ schema, uiSchema });
336
+ ```
337
+
338
+ 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).
339
+
340
+
178
341
  ### Using custom components
179
342
 
180
343
  Form components (such as wrappers) can be overridden with custom ones
@@ -1061,3 +1224,27 @@ const uiSchema: UiSchema = {
1061
1224
  hideSubmit={true} // hides submit-button of the form
1062
1225
  />
1063
1226
  ```
1227
+
1228
+ ### Special handling of conflicting keys in uiSchema
1229
+
1230
+ In case one of your property names matches a reserved key in `uiSchema` (e.g. `ui:description`), you can use the syntax `> propertyName` to target the property.
1231
+
1232
+ ```tsx
1233
+ const schema = {
1234
+ type: "object",
1235
+ properties: {
1236
+ "ui:description": { type: "string", title: "Description" },
1237
+ },
1238
+ };
1239
+
1240
+ const uiSchema = {
1241
+ // This would be interpreted as the description for the parent object
1242
+ "ui:description": "Form description",
1243
+
1244
+ // This targets the property named "ui:description"
1245
+ "> ui:description": {
1246
+ "ui:placeholder": "Enter description...",
1247
+ },
1248
+ };
1249
+ ```
1250
+
@@ -12,7 +12,7 @@ const types_1 = require("./types");
12
12
  const DEFAULT_FIELD = {
13
13
  type: "string",
14
14
  _fieldType: "string",
15
- "ui:title": "New Field",
15
+ "ui:title": "",
16
16
  };
17
17
  const useObjectField = (name) => {
18
18
  if (!name) {
@@ -73,7 +73,11 @@ const ObjectField = (props) => {
73
73
  const childSchema = properties?.[child];
74
74
  if (!childSchema)
75
75
  return undefined;
76
- return ((0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(child), name: name ? [name, child].join(".") : child, schema: prepareChildSchema(childSchema, schema), uiSchema: uiSchema?.[child] }, child));
76
+ const childUiSchema = {
77
+ ...(uiSchema?.properties ?? {}),
78
+ ...(uiSchema?.[`> ${child}`] ?? uiSchema?.[child] ?? {}),
79
+ };
80
+ return ((0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(child), name: name ? [name, child].join(".") : child, schema: prepareChildSchema(childSchema, schema), uiSchema: childUiSchema }, child));
77
81
  }
78
82
  else {
79
83
  const { title, fields, collapsible } = child;
@@ -81,7 +85,13 @@ const ObjectField = (props) => {
81
85
  const childSchema = properties?.[child2];
82
86
  if (!childSchema)
83
87
  return undefined;
84
- return ((0, jsx_runtime_1.jsx)(SchemaField, { name: name ? [name, child2].join(".") : child2, schema: prepareChildSchema(childSchema, schema), uiSchema: uiSchema?.[child2] }, child2));
88
+ const childUiSchema = {
89
+ ...(uiSchema?.properties ?? {}),
90
+ ...(uiSchema?.[`> ${child2}`] ??
91
+ uiSchema?.[child2] ??
92
+ {}),
93
+ };
94
+ return ((0, jsx_runtime_1.jsx)(SchemaField, { name: name ? [name, child2].join(".") : child2, schema: prepareChildSchema(childSchema, schema), uiSchema: childUiSchema }, child2));
85
95
  }) }, fields.join("_")));
86
96
  }
87
97
  })
@@ -89,7 +99,10 @@ const ObjectField = (props) => {
89
99
  const childSchema = properties?.[child];
90
100
  if (!childSchema)
91
101
  return undefined;
92
- const childUiSchema = uiSchema?.[child];
102
+ const childUiSchema = {
103
+ ...(uiSchema?.properties ?? {}),
104
+ ...(uiSchema?.[`> ${child}`] ?? uiSchema?.[child] ?? {}),
105
+ };
93
106
  return ((0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(child), name: name ? [name, child].join(".") : child, schema: prepareChildSchema(childSchema, schema), uiSchema: {
94
107
  ...childUiSchema,
95
108
  props: {
@@ -105,12 +118,12 @@ const ObjectField = (props) => {
105
118
  }, style: { display: "none" } }, child));
106
119
  }), showAdditionalProperties &&
107
120
  orderedAdditionalProperties.map((childName, index) => {
108
- let correctUiSchema = {
121
+ const childUiSchema = {
109
122
  ...(uiSchema?.additionalProperties ?? {}),
110
123
  ...(uiSchema?.properties ?? {}),
111
- ...(uiSchema?.[childName] ?? {}),
124
+ ...(uiSchema?.[`> ${childName}`] ?? uiSchema?.[childName] ?? {}),
112
125
  };
113
- return ((0, jsx_runtime_1.jsxs)(ObjectFieldItem, { children: [(0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: correctUiSchema }, childName), (0, jsx_runtime_1.jsxs)(ObjectFieldItemButtons, { children: [onAdditionalPropertyOrderChange &&
126
+ return ((0, jsx_runtime_1.jsxs)(ObjectFieldItem, { children: [(0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: childUiSchema }, childName), (0, jsx_runtime_1.jsxs)(ObjectFieldItemButtons, { children: [onAdditionalPropertyOrderChange &&
114
127
  index > 0 &&
115
128
  getObjectFieldButton({
116
129
  purpose: types_1.ObjectFieldButtonPurpose.MOVE_UP,
@@ -140,15 +153,12 @@ const ObjectField = (props) => {
140
153
  })] })] }, childName));
141
154
  }), showAdditionalProperties &&
142
155
  hiddenAdditionalProperties.map((childName) => {
143
- let correctUiSchema = uiSchema?.[childName];
144
- if (uiSchema.properties?.["ui:order"] !== undefined &&
145
- uiSchema?.[childName]?.["ui:order"] === undefined) {
146
- correctUiSchema = {
147
- ...correctUiSchema,
148
- "ui:order": uiSchema.properties?.["ui:order"],
149
- };
150
- }
151
- return ((0, jsx_runtime_1.jsx)(ObjectFieldItem, { style: { display: "none" }, children: (0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: correctUiSchema }, childName) }, childName));
156
+ const childUiSchema = {
157
+ ...(uiSchema?.additionalProperties ?? {}),
158
+ ...(uiSchema?.properties ?? {}),
159
+ ...(uiSchema?.[`> ${childName}`] ?? uiSchema?.[childName] ?? {}),
160
+ };
161
+ return ((0, jsx_runtime_1.jsx)(ObjectFieldItem, { style: { display: "none" }, children: (0, jsx_runtime_1.jsx)(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: childUiSchema }, childName) }, childName));
152
162
  }), additionalProperties &&
153
163
  getObjectFieldButton({
154
164
  purpose: types_1.ObjectFieldButtonPurpose.ADD,
@@ -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,10 +173,22 @@ function getYupType(schema) {
156
173
  throw new Error(`createValidationSchema: Unsupported schema type: ${type}`);
157
174
  }
158
175
  }
159
- function createValidationSchema(schema, definitions = {}, conditionsContext = {}, condition) {
176
+ function createValidationSchema(schemas, definitions = {}, options) {
177
+ const { schema, uiSchema = {} } = schemas;
178
+ const { localize, conditionsContext = {}, condition } = options ?? {};
160
179
  if (!schema?.properties) {
161
180
  return Yup.object().shape({});
162
181
  }
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;
191
+ };
163
192
  const schemaDefinitions = {
164
193
  ...definitions,
165
194
  ...(schema.definitions ?? {}),
@@ -170,8 +199,15 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
170
199
  const isRequired = requiredFields?.includes(key) || required === true;
171
200
  let type = schema.type;
172
201
  let yupSchema;
202
+ const fieldUiSchema = uiSchema?.[key];
173
203
  if (schema.properties) {
174
- yupSchema = createValidationSchema(schema, schemaDefinitions, conditionsContext);
204
+ yupSchema = createValidationSchema({
205
+ schema,
206
+ uiSchema: fieldUiSchema
207
+ }, schemaDefinitions, {
208
+ localize,
209
+ conditionsContext,
210
+ });
175
211
  type = type ?? "object";
176
212
  }
177
213
  else if (type === "array" && schema.anyOf) {
@@ -186,7 +222,9 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
186
222
  return Yup.mixed().oneOf(anyOf.map((item) => item.const));
187
223
  }));
188
224
  // In case the property is required, there should be at least one item in the array
189
- yupSchema = isRequired ? anyOfSchema.min(1, "Required") : anyOfSchema;
225
+ yupSchema = isRequired
226
+ ? anyOfSchema.min(1, getErrorMessage("required", fieldUiSchema))
227
+ : anyOfSchema;
190
228
  }
191
229
  }
192
230
  else if (type === "array") {
@@ -218,8 +256,99 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
218
256
  }
219
257
  if (type === "null")
220
258
  yupSchema.nullable();
259
+ // Apply JSON Schema validation keywords based on type
260
+ if (type === "string") {
261
+ let stringSchema = yupSchema;
262
+ if (schema.minLength !== undefined) {
263
+ stringSchema = stringSchema.min(schema.minLength, getErrorMessage("minLength", fieldUiSchema, { limit: schema.minLength }));
264
+ }
265
+ if (schema.maxLength !== undefined) {
266
+ stringSchema = stringSchema.max(schema.maxLength, getErrorMessage("maxLength", fieldUiSchema, { limit: schema.maxLength }));
267
+ }
268
+ if (schema.pattern !== undefined) {
269
+ stringSchema = stringSchema.matches(new RegExp(schema.pattern), getErrorMessage("pattern", fieldUiSchema));
270
+ }
271
+ yupSchema = stringSchema;
272
+ }
273
+ if (type === "number" || type === "integer") {
274
+ let numberSchema = yupSchema;
275
+ if (schema.minimum !== undefined) {
276
+ numberSchema = numberSchema.min(schema.minimum, getErrorMessage("minimum", fieldUiSchema, { min: schema.minimum }));
277
+ }
278
+ if (schema.maximum !== undefined) {
279
+ numberSchema = numberSchema.max(schema.maximum, getErrorMessage("maximum", fieldUiSchema, { max: schema.maximum }));
280
+ }
281
+ if (schema.exclusiveMinimum !== undefined) {
282
+ numberSchema = numberSchema.moreThan(schema.exclusiveMinimum, getErrorMessage("exclusiveMinimum", fieldUiSchema, { min: schema.exclusiveMinimum }));
283
+ }
284
+ if (schema.exclusiveMaximum !== undefined) {
285
+ numberSchema = numberSchema.lessThan(schema.exclusiveMaximum, getErrorMessage("exclusiveMaximum", fieldUiSchema, { max: schema.exclusiveMaximum }));
286
+ }
287
+ if (schema.multipleOf !== undefined) {
288
+ numberSchema = numberSchema.test("multipleOf", getErrorMessage("multipleOf", fieldUiSchema, { multipleOf: schema.multipleOf }), (value) => value === undefined ||
289
+ value === null ||
290
+ value % schema.multipleOf === 0);
291
+ }
292
+ yupSchema = numberSchema;
293
+ }
294
+ if (type === "array") {
295
+ const applyArrayTests = (s) => {
296
+ let result = s;
297
+ if (schema.minItems !== undefined) {
298
+ result = result.test("minItems", getErrorMessage("minItems", fieldUiSchema, { min: schema.minItems }), (value) => !value || value.length >= schema.minItems);
299
+ }
300
+ if (schema.maxItems !== undefined) {
301
+ result = result.test("maxItems", getErrorMessage("maxItems", fieldUiSchema, { max: schema.maxItems }), (value) => !value || value.length <= schema.maxItems);
302
+ }
303
+ if (schema.uniqueItems === true) {
304
+ result = result.test("uniqueItems", getErrorMessage("uniqueItems", fieldUiSchema), (value) => {
305
+ if (!value)
306
+ return true;
307
+ return new Set(value).size === value.length;
308
+ });
309
+ }
310
+ return result;
311
+ };
312
+ if (yupSchema.test) {
313
+ yupSchema = applyArrayTests(yupSchema);
314
+ }
315
+ else {
316
+ // It's likely a Lazy schema (from mixedOf). Wrap it to apply tests.
317
+ const originalLazy = yupSchema;
318
+ yupSchema = Yup.lazy((value, options) => {
319
+ return applyArrayTests(originalLazy.resolve(options));
320
+ });
321
+ }
322
+ }
323
+ const isOptionEnum = schema.enum !== undefined;
324
+ const isOptionConst = schema.const !== undefined;
325
+ if (isOptionEnum || isOptionConst) {
326
+ // Check for $data reference (cross-field validation)
327
+ const constValue = schema.const;
328
+ const isDataRef = typeof constValue === "object" && constValue?.$data;
329
+ if (isDataRef) {
330
+ // Cross-field reference: const: { $data: "1/otherFieldName" }
331
+ // JSON Pointer format: "1/fieldName" means go up 1 level and reference fieldName
332
+ const dataPath = constValue.$data;
333
+ const pathParts = dataPath.split("/");
334
+ const refFieldName = pathParts[pathParts.length - 1]; // Get the field name (last part)
335
+ yupSchema = yupSchema.oneOf([Yup.ref(refFieldName)], getErrorMessage("const", fieldUiSchema));
336
+ }
337
+ else {
338
+ // Standard const or enum validation
339
+ const allowedValues = isOptionConst ? [schema.const] : schema.enum;
340
+ const errorKey = isOptionConst ? "const" : "enum";
341
+ yupSchema = yupSchema.oneOf(allowedValues, getErrorMessage(errorKey, fieldUiSchema));
342
+ }
343
+ }
344
+ if (schema.not &&
345
+ typeof schema.not !== "boolean" &&
346
+ schema.not.const !== undefined) {
347
+ yupSchema = yupSchema.notOneOf([schema.not.const], getErrorMessage("notConst", fieldUiSchema));
348
+ }
349
+ // JSON Schema validation keywords
221
350
  if (isRequired && type !== "array") {
222
- yupSchema = yupSchema.required("Required");
351
+ yupSchema = yupSchema.required(getErrorMessage("required", fieldUiSchema));
223
352
  }
224
353
  if (!condition) {
225
354
  return yupSchema;
@@ -257,6 +386,13 @@ function createValidationSchema(schema, definitions = {}, conditionsContext = {}
257
386
  override ones in the base, including type, presence and nullability."
258
387
  Source: https://github.com/jquense/yup?tab=readme-ov-file#schemaconcatschema-schema-schema
259
388
  */
260
- return validationSchema.concat(createValidationSchema(solvedSchemas.schema, schemaDefinitions, conditionsContext, conditionalValidation.condition));
389
+ return validationSchema.concat(createValidationSchema({
390
+ schema: solvedSchemas.schema,
391
+ uiSchema
392
+ }, schemaDefinitions, {
393
+ localize,
394
+ conditionsContext,
395
+ condition: conditionalValidation.condition
396
+ }));
261
397
  }, baseValidationSchema);
262
398
  }
@@ -9,7 +9,7 @@ import { ObjectFieldButtonPurpose } from "./types";
9
9
  const DEFAULT_FIELD = {
10
10
  type: "string",
11
11
  _fieldType: "string",
12
- "ui:title": "New Field",
12
+ "ui:title": "",
13
13
  };
14
14
  const useObjectField = (name) => {
15
15
  if (!name) {
@@ -70,7 +70,11 @@ export const ObjectField = (props) => {
70
70
  const childSchema = properties?.[child];
71
71
  if (!childSchema)
72
72
  return undefined;
73
- return (_jsx(SchemaField, { required: required?.includes(child), name: name ? [name, child].join(".") : child, schema: prepareChildSchema(childSchema, schema), uiSchema: uiSchema?.[child] }, child));
73
+ const childUiSchema = {
74
+ ...(uiSchema?.properties ?? {}),
75
+ ...(uiSchema?.[`> ${child}`] ?? uiSchema?.[child] ?? {}),
76
+ };
77
+ return (_jsx(SchemaField, { required: required?.includes(child), name: name ? [name, child].join(".") : child, schema: prepareChildSchema(childSchema, schema), uiSchema: childUiSchema }, child));
74
78
  }
75
79
  else {
76
80
  const { title, fields, collapsible } = child;
@@ -78,7 +82,13 @@ export const ObjectField = (props) => {
78
82
  const childSchema = properties?.[child2];
79
83
  if (!childSchema)
80
84
  return undefined;
81
- return (_jsx(SchemaField, { name: name ? [name, child2].join(".") : child2, schema: prepareChildSchema(childSchema, schema), uiSchema: uiSchema?.[child2] }, child2));
85
+ const childUiSchema = {
86
+ ...(uiSchema?.properties ?? {}),
87
+ ...(uiSchema?.[`> ${child2}`] ??
88
+ uiSchema?.[child2] ??
89
+ {}),
90
+ };
91
+ return (_jsx(SchemaField, { name: name ? [name, child2].join(".") : child2, schema: prepareChildSchema(childSchema, schema), uiSchema: childUiSchema }, child2));
82
92
  }) }, fields.join("_")));
83
93
  }
84
94
  })
@@ -86,7 +96,10 @@ export const ObjectField = (props) => {
86
96
  const childSchema = properties?.[child];
87
97
  if (!childSchema)
88
98
  return undefined;
89
- const childUiSchema = uiSchema?.[child];
99
+ const childUiSchema = {
100
+ ...(uiSchema?.properties ?? {}),
101
+ ...(uiSchema?.[`> ${child}`] ?? uiSchema?.[child] ?? {}),
102
+ };
90
103
  return (_jsx(SchemaField, { required: required?.includes(child), name: name ? [name, child].join(".") : child, schema: prepareChildSchema(childSchema, schema), uiSchema: {
91
104
  ...childUiSchema,
92
105
  props: {
@@ -102,12 +115,12 @@ export const ObjectField = (props) => {
102
115
  }, style: { display: "none" } }, child));
103
116
  }), showAdditionalProperties &&
104
117
  orderedAdditionalProperties.map((childName, index) => {
105
- let correctUiSchema = {
118
+ const childUiSchema = {
106
119
  ...(uiSchema?.additionalProperties ?? {}),
107
120
  ...(uiSchema?.properties ?? {}),
108
- ...(uiSchema?.[childName] ?? {}),
121
+ ...(uiSchema?.[`> ${childName}`] ?? uiSchema?.[childName] ?? {}),
109
122
  };
110
- return (_jsxs(ObjectFieldItem, { children: [_jsx(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: correctUiSchema }, childName), _jsxs(ObjectFieldItemButtons, { children: [onAdditionalPropertyOrderChange &&
123
+ return (_jsxs(ObjectFieldItem, { children: [_jsx(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: childUiSchema }, childName), _jsxs(ObjectFieldItemButtons, { children: [onAdditionalPropertyOrderChange &&
111
124
  index > 0 &&
112
125
  getObjectFieldButton({
113
126
  purpose: ObjectFieldButtonPurpose.MOVE_UP,
@@ -137,15 +150,12 @@ export const ObjectField = (props) => {
137
150
  })] })] }, childName));
138
151
  }), showAdditionalProperties &&
139
152
  hiddenAdditionalProperties.map((childName) => {
140
- let correctUiSchema = uiSchema?.[childName];
141
- if (uiSchema.properties?.["ui:order"] !== undefined &&
142
- uiSchema?.[childName]?.["ui:order"] === undefined) {
143
- correctUiSchema = {
144
- ...correctUiSchema,
145
- "ui:order": uiSchema.properties?.["ui:order"],
146
- };
147
- }
148
- return (_jsx(ObjectFieldItem, { style: { display: "none" }, children: _jsx(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: correctUiSchema }, childName) }, childName));
153
+ const childUiSchema = {
154
+ ...(uiSchema?.additionalProperties ?? {}),
155
+ ...(uiSchema?.properties ?? {}),
156
+ ...(uiSchema?.[`> ${childName}`] ?? uiSchema?.[childName] ?? {}),
157
+ };
158
+ return (_jsx(ObjectFieldItem, { style: { display: "none" }, children: _jsx(SchemaField, { required: required?.includes(childName), name: name ? [name, childName].join(".") : childName, schema: prepareChildSchema(schema.additionalProperties, schema), uiSchema: childUiSchema }, childName) }, childName));
149
159
  }), additionalProperties &&
150
160
  getObjectFieldButton({
151
161
  purpose: ObjectFieldButtonPurpose.ADD,
@@ -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,10 +137,22 @@ function getYupType(schema) {
120
137
  throw new Error(`createValidationSchema: Unsupported schema type: ${type}`);
121
138
  }
122
139
  }
123
- export function createValidationSchema(schema, definitions = {}, conditionsContext = {}, condition) {
140
+ export function createValidationSchema(schemas, definitions = {}, options) {
141
+ const { schema, uiSchema = {} } = schemas;
142
+ const { localize, conditionsContext = {}, condition } = options ?? {};
124
143
  if (!schema?.properties) {
125
144
  return Yup.object().shape({});
126
145
  }
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;
155
+ };
127
156
  const schemaDefinitions = {
128
157
  ...definitions,
129
158
  ...(schema.definitions ?? {}),
@@ -134,8 +163,15 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
134
163
  const isRequired = requiredFields?.includes(key) || required === true;
135
164
  let type = schema.type;
136
165
  let yupSchema;
166
+ const fieldUiSchema = uiSchema?.[key];
137
167
  if (schema.properties) {
138
- yupSchema = createValidationSchema(schema, schemaDefinitions, conditionsContext);
168
+ yupSchema = createValidationSchema({
169
+ schema,
170
+ uiSchema: fieldUiSchema
171
+ }, schemaDefinitions, {
172
+ localize,
173
+ conditionsContext,
174
+ });
139
175
  type = type ?? "object";
140
176
  }
141
177
  else if (type === "array" && schema.anyOf) {
@@ -150,7 +186,9 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
150
186
  return Yup.mixed().oneOf(anyOf.map((item) => item.const));
151
187
  }));
152
188
  // In case the property is required, there should be at least one item in the array
153
- yupSchema = isRequired ? anyOfSchema.min(1, "Required") : anyOfSchema;
189
+ yupSchema = isRequired
190
+ ? anyOfSchema.min(1, getErrorMessage("required", fieldUiSchema))
191
+ : anyOfSchema;
154
192
  }
155
193
  }
156
194
  else if (type === "array") {
@@ -182,8 +220,99 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
182
220
  }
183
221
  if (type === "null")
184
222
  yupSchema.nullable();
223
+ // Apply JSON Schema validation keywords based on type
224
+ if (type === "string") {
225
+ let stringSchema = yupSchema;
226
+ if (schema.minLength !== undefined) {
227
+ stringSchema = stringSchema.min(schema.minLength, getErrorMessage("minLength", fieldUiSchema, { limit: schema.minLength }));
228
+ }
229
+ if (schema.maxLength !== undefined) {
230
+ stringSchema = stringSchema.max(schema.maxLength, getErrorMessage("maxLength", fieldUiSchema, { limit: schema.maxLength }));
231
+ }
232
+ if (schema.pattern !== undefined) {
233
+ stringSchema = stringSchema.matches(new RegExp(schema.pattern), getErrorMessage("pattern", fieldUiSchema));
234
+ }
235
+ yupSchema = stringSchema;
236
+ }
237
+ if (type === "number" || type === "integer") {
238
+ let numberSchema = yupSchema;
239
+ if (schema.minimum !== undefined) {
240
+ numberSchema = numberSchema.min(schema.minimum, getErrorMessage("minimum", fieldUiSchema, { min: schema.minimum }));
241
+ }
242
+ if (schema.maximum !== undefined) {
243
+ numberSchema = numberSchema.max(schema.maximum, getErrorMessage("maximum", fieldUiSchema, { max: schema.maximum }));
244
+ }
245
+ if (schema.exclusiveMinimum !== undefined) {
246
+ numberSchema = numberSchema.moreThan(schema.exclusiveMinimum, getErrorMessage("exclusiveMinimum", fieldUiSchema, { min: schema.exclusiveMinimum }));
247
+ }
248
+ if (schema.exclusiveMaximum !== undefined) {
249
+ numberSchema = numberSchema.lessThan(schema.exclusiveMaximum, getErrorMessage("exclusiveMaximum", fieldUiSchema, { max: schema.exclusiveMaximum }));
250
+ }
251
+ if (schema.multipleOf !== undefined) {
252
+ numberSchema = numberSchema.test("multipleOf", getErrorMessage("multipleOf", fieldUiSchema, { multipleOf: schema.multipleOf }), (value) => value === undefined ||
253
+ value === null ||
254
+ value % schema.multipleOf === 0);
255
+ }
256
+ yupSchema = numberSchema;
257
+ }
258
+ if (type === "array") {
259
+ const applyArrayTests = (s) => {
260
+ let result = s;
261
+ if (schema.minItems !== undefined) {
262
+ result = result.test("minItems", getErrorMessage("minItems", fieldUiSchema, { min: schema.minItems }), (value) => !value || value.length >= schema.minItems);
263
+ }
264
+ if (schema.maxItems !== undefined) {
265
+ result = result.test("maxItems", getErrorMessage("maxItems", fieldUiSchema, { max: schema.maxItems }), (value) => !value || value.length <= schema.maxItems);
266
+ }
267
+ if (schema.uniqueItems === true) {
268
+ result = result.test("uniqueItems", getErrorMessage("uniqueItems", fieldUiSchema), (value) => {
269
+ if (!value)
270
+ return true;
271
+ return new Set(value).size === value.length;
272
+ });
273
+ }
274
+ return result;
275
+ };
276
+ if (yupSchema.test) {
277
+ yupSchema = applyArrayTests(yupSchema);
278
+ }
279
+ else {
280
+ // It's likely a Lazy schema (from mixedOf). Wrap it to apply tests.
281
+ const originalLazy = yupSchema;
282
+ yupSchema = Yup.lazy((value, options) => {
283
+ return applyArrayTests(originalLazy.resolve(options));
284
+ });
285
+ }
286
+ }
287
+ const isOptionEnum = schema.enum !== undefined;
288
+ const isOptionConst = schema.const !== undefined;
289
+ if (isOptionEnum || isOptionConst) {
290
+ // Check for $data reference (cross-field validation)
291
+ const constValue = schema.const;
292
+ const isDataRef = typeof constValue === "object" && constValue?.$data;
293
+ if (isDataRef) {
294
+ // Cross-field reference: const: { $data: "1/otherFieldName" }
295
+ // JSON Pointer format: "1/fieldName" means go up 1 level and reference fieldName
296
+ const dataPath = constValue.$data;
297
+ const pathParts = dataPath.split("/");
298
+ const refFieldName = pathParts[pathParts.length - 1]; // Get the field name (last part)
299
+ yupSchema = yupSchema.oneOf([Yup.ref(refFieldName)], getErrorMessage("const", fieldUiSchema));
300
+ }
301
+ else {
302
+ // Standard const or enum validation
303
+ const allowedValues = isOptionConst ? [schema.const] : schema.enum;
304
+ const errorKey = isOptionConst ? "const" : "enum";
305
+ yupSchema = yupSchema.oneOf(allowedValues, getErrorMessage(errorKey, fieldUiSchema));
306
+ }
307
+ }
308
+ if (schema.not &&
309
+ typeof schema.not !== "boolean" &&
310
+ schema.not.const !== undefined) {
311
+ yupSchema = yupSchema.notOneOf([schema.not.const], getErrorMessage("notConst", fieldUiSchema));
312
+ }
313
+ // JSON Schema validation keywords
185
314
  if (isRequired && type !== "array") {
186
- yupSchema = yupSchema.required("Required");
315
+ yupSchema = yupSchema.required(getErrorMessage("required", fieldUiSchema));
187
316
  }
188
317
  if (!condition) {
189
318
  return yupSchema;
@@ -221,6 +350,13 @@ export function createValidationSchema(schema, definitions = {}, conditionsConte
221
350
  override ones in the base, including type, presence and nullability."
222
351
  Source: https://github.com/jquense/yup?tab=readme-ov-file#schemaconcatschema-schema-schema
223
352
  */
224
- return validationSchema.concat(createValidationSchema(solvedSchemas.schema, schemaDefinitions, conditionsContext, conditionalValidation.condition));
353
+ return validationSchema.concat(createValidationSchema({
354
+ schema: solvedSchemas.schema,
355
+ uiSchema
356
+ }, schemaDefinitions, {
357
+ localize,
358
+ conditionsContext,
359
+ condition: conditionalValidation.condition
360
+ }));
225
361
  }, baseValidationSchema);
226
362
  }
@@ -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,12 @@ type ConditionsContext = {
17
19
  condition: Condition;
18
20
  yupSchema: Yup.Schema<any, any, any, "">;
19
21
  };
20
- export declare function createValidationSchema(schema: Schema, definitions?: Schema, conditionsContext?: Record<string, ConditionsContext[]>, condition?: Condition): Yup.ObjectSchema<{}, Yup.AnyObject, {}, "">;
22
+ export declare function createValidationSchema(schemas: {
23
+ schema: Schema;
24
+ uiSchema?: UiSchema;
25
+ }, definitions?: Schema, options?: {
26
+ localize?: LocalizeType;
27
+ conditionsContext?: Record<string, ConditionsContext[]>;
28
+ condition?: Condition;
29
+ }): Yup.ObjectSchema<{}, Yup.AnyObject, {}, "">;
21
30
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nolwenture/formik-jsonschema-form",
3
- "version": "1.4.3",
3
+ "version": "2.0.0",
4
4
  "description": "Formik form generation from json schema",
5
5
  "author": "anttivikman <antti.vikman@nolwenture.com>",
6
6
  "contributors": [