Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/input_schema/src/input_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,48 @@ const [fieldDefinitions, subFieldDefinitions] = Object
return acc;
}, [[], []]);

/**
* Retrieves a custom error message defined in the schema for a particular schema path.
* @param rootSchema json schema object
* @param schemaPath schema path to the failed validation keyword,
* as provided in an AJV error object, including the keyword at the end, e.g. "#/properties/name/type"
*/
export function getCustomErrorMessage(rootSchema: Record<string, any>, schemaPath: string): string | null {
if (!schemaPath) return null;

const pathParts = schemaPath
.replace(/^#\//, '')
.split('/')
.filter(Boolean);
Comment on lines +52 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I was wondering how AJV deals with / in schemaPath and it seems it escapes it as ~1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I did try it too :)


// The last part is the keyword
const keyword = pathParts.pop();
if (!keyword) return null;

// Navigate through the schema to find the relevant fragment
let schemaFragment: Record<string, any> = rootSchema;
for (const key of pathParts) {
if (schemaFragment && typeof schemaFragment === 'object') {
schemaFragment = schemaFragment[key];
} else {
return null;
}
}

if (typeof schemaFragment !== 'object') {
return null;
}

const { errorMessage } = schemaFragment;
if (!errorMessage) return null;

if (typeof errorMessage === 'object' && keyword in errorMessage) {
return errorMessage[keyword];
}

return null;
}

/**
* This function parses AJV error and transforms it into a readable string.
*
Expand Down Expand Up @@ -68,6 +110,14 @@ export function parseAjvError(
return name.replace(/^\/|\/$/g, '').replace(/\//g, '.');
};

// First, try to get a custom error message from the schema
// If found, use it directly and skip further processing
const customError = getCustomErrorMessage({ properties }, error.schemaPath);
if (customError) {
fieldKey = cleanPropertyName(error.instancePath);
return { fieldKey, message: customError };
}

// If error is with keyword type, it means that type of input is incorrect
// this can mean that provided value is null
if (error.keyword === 'type') {
Expand Down
17 changes: 11 additions & 6 deletions packages/input_schema/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { countries } from 'countries-list';
import { PROXY_URL_REGEX, URL_REGEX } from '@apify/consts';
import { isEncryptedValueForFieldSchema, isEncryptedValueForFieldType } from '@apify/input_secrets';

import { parseAjvError } from './input_schema';
import { getCustomErrorMessage, parseAjvError } from './input_schema';
import { m } from './intl';

/**
Expand Down Expand Up @@ -197,7 +197,8 @@ export function validateInputUsingValidator(
if (!check.test(item.key)) invalidIndexes.push(index);
});
if (invalidIndexes.length) {
fieldErrors.push(m('inputSchema.validation.arrayKeysInvalid', {
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternKey`);
fieldErrors.push(customError ?? m('inputSchema.validation.arrayKeysInvalid', {
rootName: 'input',
fieldKey: property,
invalidIndexes: invalidIndexes.join(','),
Expand All @@ -213,7 +214,8 @@ export function validateInputUsingValidator(
if (!check.test(item.value)) invalidIndexes.push(index);
});
if (invalidIndexes.length) {
fieldErrors.push(m('inputSchema.validation.arrayValuesInvalid', {
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternValue`);
fieldErrors.push(customError ?? m('inputSchema.validation.arrayValuesInvalid', {
rootName: 'input',
fieldKey: property,
invalidIndexes: invalidIndexes.join(','),
Expand All @@ -228,7 +230,8 @@ export function validateInputUsingValidator(
if (!check.test(item)) invalidIndexes.push(index);
});
if (invalidIndexes.length) {
fieldErrors.push(m('inputSchema.validation.arrayValuesInvalid', {
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternValue`);
fieldErrors.push(customError ?? m('inputSchema.validation.arrayValuesInvalid', {
rootName: 'input',
fieldKey: property,
invalidIndexes: invalidIndexes.join(','),
Expand All @@ -246,7 +249,8 @@ export function validateInputUsingValidator(
if (!check.test(key)) invalidKeys.push(key);
});
if (invalidKeys.length) {
fieldErrors.push(m('inputSchema.validation.objectKeysInvalid', {
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternKey`);
fieldErrors.push(customError ?? m('inputSchema.validation.objectKeysInvalid', {
rootName: 'input',
fieldKey: property,
invalidKeys: invalidKeys.join(','),
Expand All @@ -262,7 +266,8 @@ export function validateInputUsingValidator(
if (typeof propertyValue !== 'string' || !check.test(propertyValue)) invalidKeys.push(key);
});
if (invalidKeys.length) {
fieldErrors.push(m('inputSchema.validation.objectValuesInvalid', {
const customError = getCustomErrorMessage(inputSchema, `properties/${property}/patternValue`);
fieldErrors.push(customError ?? m('inputSchema.validation.objectValuesInvalid', {
rootName: 'input',
fieldKey: property,
invalidKeys: invalidKeys.join(','),
Expand Down
87 changes: 65 additions & 22 deletions packages/json_schemas/schemas/input.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
},
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "enum"],
"if": {
Expand Down Expand Up @@ -124,7 +125,8 @@
"maxLength": { "type": "integer" },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" },
"isSecret": { "enum": [false] }
"isSecret": { "enum": [false] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"unevaluatedProperties": false,
"allOf": [
Expand Down Expand Up @@ -171,7 +173,8 @@
"editor": { "enum": ["textfield", "textarea", "hidden"] },
"isSecret": { "enum": [true] },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
}
}
},
Expand Down Expand Up @@ -206,7 +209,8 @@
"placeholderValue": { "type": "string" },
"patternKey": { "type": "string" },
"patternValue": { "type": "string" },
"isSecret": { "enum": [false] }
"isSecret": { "enum": [false] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"unevaluatedProperties": false,
"allOf": [
Expand Down Expand Up @@ -303,7 +307,8 @@
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" },
"items": { "$ref": "#/definitions/arrayItems" },
"isSecret": { "enum": [true] }
"isSecret": { "enum": [true] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
}
}
},
Expand Down Expand Up @@ -347,7 +352,8 @@
},
"additionalProperties": {
"type": "boolean"
}
},
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"unevaluatedProperties": false,
"allOf": [
Expand Down Expand Up @@ -402,7 +408,8 @@
},
"additionalProperties": {
"type": "boolean"
}
},
"errorMessage": { "$ref": "#/definitions/errorMessage" }
}
}
},
Expand All @@ -422,7 +429,8 @@
"unit": { "type": "string" },
"editor": { "enum": ["number", "hidden"] },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand Down Expand Up @@ -451,7 +459,8 @@
"unit": { "type": "string" },
"editor": { "enum": ["number", "hidden"] },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand Down Expand Up @@ -479,7 +488,8 @@
"groupDescription": { "type": "string" },
"editor": { "enum": ["checkbox", "hidden"] },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand Down Expand Up @@ -521,7 +531,8 @@
"minLength": { "type": "integer" },
"maxLength": { "type": "integer" },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "resourceType"],
"allOf": [
Expand Down Expand Up @@ -580,7 +591,8 @@
"uniqueItems": { "type": "boolean" },
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "resourceType"],
"allOf": [
Expand Down Expand Up @@ -632,7 +644,8 @@
"nullable": { "type": "boolean" },
"editor": { "enum": ["json", "hidden"] },
"sectionCaption": { "type": "string" },
"sectionDescription": { "type": "string" }
"sectionDescription": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "editor"],
"if": {
Expand Down Expand Up @@ -667,7 +680,8 @@
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
},
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "enum"],
"if": {
Expand All @@ -694,7 +708,8 @@
"nullable": { "type": "boolean" },
"minLength": { "type": "integer" },
"maxLength": { "type": "integer" },
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "fileupload"] }
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "fileupload"] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"allOf": [
Expand Down Expand Up @@ -746,7 +761,8 @@
"placeholderKey": { "type": "string" },
"placeholderValue": { "type": "string" },
"patternKey": { "type": "string" },
"patternValue": { "type": "string" }
"patternValue": { "type": "string" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"unevaluatedProperties": false,
Expand Down Expand Up @@ -842,7 +858,8 @@
"minimum": { "type": "integer" },
"maximum": { "type": "integer" },
"unit": { "type": "string" },
"editor": { "enum": ["number", "hidden"] }
"editor": { "enum": ["number", "hidden"] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand All @@ -869,7 +886,8 @@
"minimum": { "type": "number" },
"maximum": { "type": "number" },
"unit": { "type": "string" },
"editor": { "enum": ["number", "hidden"] }
"editor": { "enum": ["number", "hidden"] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand All @@ -895,7 +913,8 @@
"nullable": { "type": "boolean" },
"groupCaption": { "type": "string" },
"groupDescription": { "type": "string" },
"editor": { "enum": ["checkbox", "hidden"] }
"editor": { "enum": ["checkbox", "hidden"] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand Down Expand Up @@ -933,7 +952,8 @@
},
"additionalProperties": {
"type": "boolean"
}
},
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
"if": {
Expand Down Expand Up @@ -973,7 +993,8 @@
"nullable": { "type": "boolean" },
"pattern": { "type": "string" },
"minLength": { "type": "integer" },
"maxLength": { "type": "integer" }
"maxLength": { "type": "integer" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "resourceType"],
"if": {
Expand Down Expand Up @@ -1013,7 +1034,8 @@
"minItems": { "type": "integer" },
"maxItems": { "type": "integer" },
"uniqueItems": { "type": "boolean" },
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] }
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description", "resourceType"],
"if": {
Expand Down Expand Up @@ -1548,6 +1570,27 @@
"required": ["type"]
}
}
},
"errorMessage": {
"title": "Utils: Custom error message definition",
"type": "object",
"properties": {
"type": { "type": "string" },
"pattern": { "type": "string" },
"minLength": { "type": "string" },
"maxLength": { "type": "string" },
"enum": { "type": "string" },
"minimum": { "type": "string" },
"maximum": { "type": "string" },
"minItems": { "type": "string" },
"maxItems": { "type": "string" },
"uniqueItems": { "type": "string" },
"minProperties": { "type": "string" },
"maxProperties": { "type": "string" },
"patternKey": { "type": "string" },
"patternValue": { "type": "string" }
},
"additionalProperties": false
}
}
}
Loading