Skip to content

Commit a0104c3

Browse files
committed
feat(input_schema): Custom error messages
1 parent f05abd8 commit a0104c3

File tree

5 files changed

+364
-28
lines changed

5 files changed

+364
-28
lines changed

packages/input_schema/src/input_schema.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,48 @@ const [fieldDefinitions, subFieldDefinitions] = Object
4040
return acc;
4141
}, [[], []]);
4242

43+
/**
44+
* Retrieves a custom error message defined in the schema for a particular schema path.
45+
* @param rootSchema json schema object
46+
* @param schemaPath schema path to the failed validation keyword,
47+
* as provided in an AJV error object, including the keyword at the end, e.g. "#/properties/name/type"
48+
*/
49+
export function getCustomErrorMessage(rootSchema: Record<string, any>, schemaPath: string): string | null {
50+
if (!schemaPath) return null;
51+
52+
const pathParts = schemaPath
53+
.replace(/^#\//, '')
54+
.split('/')
55+
.filter(Boolean);
56+
57+
// The last part is the keyword
58+
const keyword = pathParts.pop();
59+
if (!keyword) return null;
60+
61+
// Navigate through the schema to find the relevant fragment
62+
let schemaFragment: Record<string, any> = rootSchema;
63+
for (const key of pathParts) {
64+
if (schemaFragment && typeof schemaFragment === 'object') {
65+
schemaFragment = schemaFragment[key];
66+
} else {
67+
return null;
68+
}
69+
}
70+
71+
if (typeof schemaFragment !== 'object') {
72+
return null;
73+
}
74+
75+
const { errorMessage } = schemaFragment;
76+
if (!errorMessage) return null;
77+
78+
if (typeof errorMessage === 'object' && keyword in errorMessage) {
79+
return errorMessage[keyword];
80+
}
81+
82+
return null;
83+
}
84+
4385
/**
4486
* This function parses AJV error and transforms it into a readable string.
4587
*
@@ -68,6 +110,14 @@ export function parseAjvError(
68110
return name.replace(/^\/|\/$/g, '').replace(/\//g, '.');
69111
};
70112

113+
// First, try to get a custom error message from the schema
114+
// If found, use it directly and skip further processing
115+
const customError = getCustomErrorMessage({ properties }, error.schemaPath);
116+
if (customError) {
117+
fieldKey = cleanPropertyName(error.instancePath);
118+
return { fieldKey, message: customError };
119+
}
120+
71121
// If error is with keyword type, it means that type of input is incorrect
72122
// this can mean that provided value is null
73123
if (error.keyword === 'type') {

packages/input_schema/src/utilities.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { countries } from 'countries-list';
66
import { PROXY_URL_REGEX, URL_REGEX } from '@apify/consts';
77
import { isEncryptedValueForFieldSchema, isEncryptedValueForFieldType } from '@apify/input_secrets';
88

9-
import { parseAjvError } from './input_schema';
9+
import { getCustomErrorMessage, parseAjvError } from './input_schema';
1010
import { m } from './intl';
1111

1212
/**
@@ -197,7 +197,8 @@ export function validateInputUsingValidator(
197197
if (!check.test(item.key)) invalidIndexes.push(index);
198198
});
199199
if (invalidIndexes.length) {
200-
fieldErrors.push(m('inputSchema.validation.arrayKeysInvalid', {
200+
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternKey`);
201+
fieldErrors.push(customError ?? m('inputSchema.validation.arrayKeysInvalid', {
201202
rootName: 'input',
202203
fieldKey: property,
203204
invalidIndexes: invalidIndexes.join(','),
@@ -213,7 +214,8 @@ export function validateInputUsingValidator(
213214
if (!check.test(item.value)) invalidIndexes.push(index);
214215
});
215216
if (invalidIndexes.length) {
216-
fieldErrors.push(m('inputSchema.validation.arrayValuesInvalid', {
217+
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternValue`);
218+
fieldErrors.push(customError ?? m('inputSchema.validation.arrayValuesInvalid', {
217219
rootName: 'input',
218220
fieldKey: property,
219221
invalidIndexes: invalidIndexes.join(','),
@@ -228,7 +230,8 @@ export function validateInputUsingValidator(
228230
if (!check.test(item)) invalidIndexes.push(index);
229231
});
230232
if (invalidIndexes.length) {
231-
fieldErrors.push(m('inputSchema.validation.arrayValuesInvalid', {
233+
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternValue`);
234+
fieldErrors.push(customError ?? m('inputSchema.validation.arrayValuesInvalid', {
232235
rootName: 'input',
233236
fieldKey: property,
234237
invalidIndexes: invalidIndexes.join(','),
@@ -246,7 +249,8 @@ export function validateInputUsingValidator(
246249
if (!check.test(key)) invalidKeys.push(key);
247250
});
248251
if (invalidKeys.length) {
249-
fieldErrors.push(m('inputSchema.validation.objectKeysInvalid', {
252+
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternKey`);
253+
fieldErrors.push(customError ?? m('inputSchema.validation.objectKeysInvalid', {
250254
rootName: 'input',
251255
fieldKey: property,
252256
invalidKeys: invalidKeys.join(','),
@@ -262,7 +266,8 @@ export function validateInputUsingValidator(
262266
if (typeof propertyValue !== 'string' || !check.test(propertyValue)) invalidKeys.push(key);
263267
});
264268
if (invalidKeys.length) {
265-
fieldErrors.push(m('inputSchema.validation.objectValuesInvalid', {
269+
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternValue`);
270+
fieldErrors.push(customError ?? m('inputSchema.validation.objectValuesInvalid', {
266271
rootName: 'input',
267272
fieldKey: property,
268273
invalidKeys: invalidKeys.join(','),

packages/json_schemas/schemas/input.schema.json

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
"type": "array",
7777
"items": { "type": "string" },
7878
"minItems": 1
79-
}
79+
},
80+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
8081
},
8182
"required": ["type", "title", "description", "enum"],
8283
"if": {
@@ -124,7 +125,8 @@
124125
"maxLength": { "type": "integer" },
125126
"sectionCaption": { "type": "string" },
126127
"sectionDescription": { "type": "string" },
127-
"isSecret": { "enum": [false] }
128+
"isSecret": { "enum": [false] },
129+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
128130
},
129131
"unevaluatedProperties": false,
130132
"allOf": [
@@ -171,7 +173,8 @@
171173
"editor": { "enum": ["textfield", "textarea", "hidden"] },
172174
"isSecret": { "enum": [true] },
173175
"sectionCaption": { "type": "string" },
174-
"sectionDescription": { "type": "string" }
176+
"sectionDescription": { "type": "string" },
177+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
175178
}
176179
}
177180
},
@@ -206,7 +209,8 @@
206209
"placeholderValue": { "type": "string" },
207210
"patternKey": { "type": "string" },
208211
"patternValue": { "type": "string" },
209-
"isSecret": { "enum": [false] }
212+
"isSecret": { "enum": [false] },
213+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
210214
},
211215
"unevaluatedProperties": false,
212216
"allOf": [
@@ -303,7 +307,8 @@
303307
"sectionCaption": { "type": "string" },
304308
"sectionDescription": { "type": "string" },
305309
"items": { "$ref": "#/definitions/arrayItems" },
306-
"isSecret": { "enum": [true] }
310+
"isSecret": { "enum": [true] },
311+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
307312
}
308313
}
309314
},
@@ -347,7 +352,8 @@
347352
},
348353
"additionalProperties": {
349354
"type": "boolean"
350-
}
355+
},
356+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
351357
},
352358
"unevaluatedProperties": false,
353359
"allOf": [
@@ -402,7 +408,8 @@
402408
},
403409
"additionalProperties": {
404410
"type": "boolean"
405-
}
411+
},
412+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
406413
}
407414
}
408415
},
@@ -422,7 +429,8 @@
422429
"unit": { "type": "string" },
423430
"editor": { "enum": ["number", "hidden"] },
424431
"sectionCaption": { "type": "string" },
425-
"sectionDescription": { "type": "string" }
432+
"sectionDescription": { "type": "string" },
433+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
426434
},
427435
"required": ["type", "title", "description"],
428436
"if": {
@@ -451,7 +459,8 @@
451459
"unit": { "type": "string" },
452460
"editor": { "enum": ["number", "hidden"] },
453461
"sectionCaption": { "type": "string" },
454-
"sectionDescription": { "type": "string" }
462+
"sectionDescription": { "type": "string" },
463+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
455464
},
456465
"required": ["type", "title", "description"],
457466
"if": {
@@ -479,7 +488,8 @@
479488
"groupDescription": { "type": "string" },
480489
"editor": { "enum": ["checkbox", "hidden"] },
481490
"sectionCaption": { "type": "string" },
482-
"sectionDescription": { "type": "string" }
491+
"sectionDescription": { "type": "string" },
492+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
483493
},
484494
"required": ["type", "title", "description"],
485495
"if": {
@@ -521,7 +531,8 @@
521531
"minLength": { "type": "integer" },
522532
"maxLength": { "type": "integer" },
523533
"sectionCaption": { "type": "string" },
524-
"sectionDescription": { "type": "string" }
534+
"sectionDescription": { "type": "string" },
535+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
525536
},
526537
"required": ["type", "title", "description", "resourceType"],
527538
"allOf": [
@@ -580,7 +591,8 @@
580591
"uniqueItems": { "type": "boolean" },
581592
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] },
582593
"sectionCaption": { "type": "string" },
583-
"sectionDescription": { "type": "string" }
594+
"sectionDescription": { "type": "string" },
595+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
584596
},
585597
"required": ["type", "title", "description", "resourceType"],
586598
"allOf": [
@@ -632,7 +644,8 @@
632644
"nullable": { "type": "boolean" },
633645
"editor": { "enum": ["json", "hidden"] },
634646
"sectionCaption": { "type": "string" },
635-
"sectionDescription": { "type": "string" }
647+
"sectionDescription": { "type": "string" },
648+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
636649
},
637650
"required": ["type", "title", "description", "editor"],
638651
"if": {
@@ -667,7 +680,8 @@
667680
"type": "array",
668681
"items": { "type": "string" },
669682
"minItems": 1
670-
}
683+
},
684+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
671685
},
672686
"required": ["type", "title", "description", "enum"],
673687
"if": {
@@ -694,7 +708,8 @@
694708
"nullable": { "type": "boolean" },
695709
"minLength": { "type": "integer" },
696710
"maxLength": { "type": "integer" },
697-
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "fileupload"] }
711+
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "fileupload"] },
712+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
698713
},
699714
"required": ["type", "title", "description"],
700715
"allOf": [
@@ -746,7 +761,8 @@
746761
"placeholderKey": { "type": "string" },
747762
"placeholderValue": { "type": "string" },
748763
"patternKey": { "type": "string" },
749-
"patternValue": { "type": "string" }
764+
"patternValue": { "type": "string" },
765+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
750766
},
751767
"required": ["type", "title", "description"],
752768
"unevaluatedProperties": false,
@@ -842,7 +858,8 @@
842858
"minimum": { "type": "integer" },
843859
"maximum": { "type": "integer" },
844860
"unit": { "type": "string" },
845-
"editor": { "enum": ["number", "hidden"] }
861+
"editor": { "enum": ["number", "hidden"] },
862+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
846863
},
847864
"required": ["type", "title", "description"],
848865
"if": {
@@ -869,7 +886,8 @@
869886
"minimum": { "type": "number" },
870887
"maximum": { "type": "number" },
871888
"unit": { "type": "string" },
872-
"editor": { "enum": ["number", "hidden"] }
889+
"editor": { "enum": ["number", "hidden"] },
890+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
873891
},
874892
"required": ["type", "title", "description"],
875893
"if": {
@@ -895,7 +913,8 @@
895913
"nullable": { "type": "boolean" },
896914
"groupCaption": { "type": "string" },
897915
"groupDescription": { "type": "string" },
898-
"editor": { "enum": ["checkbox", "hidden"] }
916+
"editor": { "enum": ["checkbox", "hidden"] },
917+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
899918
},
900919
"required": ["type", "title", "description"],
901920
"if": {
@@ -933,7 +952,8 @@
933952
},
934953
"additionalProperties": {
935954
"type": "boolean"
936-
}
955+
},
956+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
937957
},
938958
"required": ["type", "title", "description"],
939959
"if": {
@@ -973,7 +993,8 @@
973993
"nullable": { "type": "boolean" },
974994
"pattern": { "type": "string" },
975995
"minLength": { "type": "integer" },
976-
"maxLength": { "type": "integer" }
996+
"maxLength": { "type": "integer" },
997+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
977998
},
978999
"required": ["type", "title", "description", "resourceType"],
9791000
"if": {
@@ -1013,7 +1034,8 @@
10131034
"minItems": { "type": "integer" },
10141035
"maxItems": { "type": "integer" },
10151036
"uniqueItems": { "type": "boolean" },
1016-
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] }
1037+
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] },
1038+
"errorMessage": { "$ref": "#/definitions/errorMessage" }
10171039
},
10181040
"required": ["type", "title", "description", "resourceType"],
10191041
"if": {
@@ -1548,6 +1570,27 @@
15481570
"required": ["type"]
15491571
}
15501572
}
1573+
},
1574+
"errorMessage": {
1575+
"title": "Utils: Custom error message definition",
1576+
"type": "object",
1577+
"properties": {
1578+
"type": { "type": "string" },
1579+
"pattern": { "type": "string" },
1580+
"minLength": { "type": "string" },
1581+
"maxLength": { "type": "string" },
1582+
"enum": { "type": "string" },
1583+
"minimum": { "type": "string" },
1584+
"maximum": { "type": "string" },
1585+
"minItems": { "type": "string" },
1586+
"maxItems": { "type": "string" },
1587+
"uniqueItems": { "type": "string" },
1588+
"minProperties": { "type": "string" },
1589+
"maxProperties": { "type": "string" },
1590+
"patternKey": { "type": "string" },
1591+
"patternValue": { "type": "string" }
1592+
},
1593+
"additionalProperties": false
15511594
}
15521595
}
15531596
}

0 commit comments

Comments
 (0)