What version of Ajv you are you using?
4.8.2
What problem do you want to solve?
Modifying/re-formatting the validated value.
Users input all kinds of values and you wouldn't make it hard for them, just to get a well-formatted value. You should clean/modify the input if it can be validated.
What do you think is the correct solution to problem?
We could have an additional feature under Options to modify validated data — reformat:Object option.
let ajvOptions = {
...
useDefaults: true,
formats: {
"phone": function (value) {
return /^[\d\-\(\)\+ ]{11,}$/.test(value); // e.g. +90 (212) 555-1112233
}
},
reformat: {
"phone": function (value) {
return value.replace(/[^\d]/g, ''); // e.g. 902125551112233
}
},
...
};
Will you be able to implement it?
Not currently.
It may be an interesting idea.
But the implementations of custom formats will be responsible for reformatting rather than Ajv, because it is too much heuristics otherwise which is out of scope. I guess parent object and the property name can be passed to format validation function to allow them to modify the string that is validated. So no option will be necessary, users will just decide whether they want to re-format or not in the function implementation.
Should be done together with #119 I think
That's even better bec. it'll also enable relational validation... Cases such as "password cannot include username"... Or "company name is required if account type is corporate".
So you'd return a value (re-formatted or not) to validate. void to invalidate, right?
No, the function will have to modify the value in the parent object that would also be passed. Same as custom keywords do. Generally I think custom keywords are better suited for advanced validation scenarios
Thanks for the custom-keywords suggestion.
Until this is built-in, this is how I reformat the validated data:
reformat module that defines formatter methods:
const reformat = {
// remove all non-numeric chars
numeric() {
return value.replace(/[^\d]/g, '');
}
};
Schema validator:
// dep modules
import AJV from 'ajv';
import Notation from 'notation';
// own modules
import reformat from './reformat';
function _identity(v) {
return v;
};
let ajv = new AJV(options);
ajv.addKeyword('reformat', {
type: 'string',
compile(sch, parentSchema) {
// ref for formatter method
let format = reformat[sch];
// if invalid method, we'll have no errors, just return the value.
if (typeof format !== 'function') format = _identity;
return (data, dataPath, parentObject, propName, rootData) => {
// remove leading dot
let dPath = dataPath.slice(1);
// Using Notation to set/update the value on the parentObject
Notation.create(parentObject).set(dPath, format(data));
// always return true (for validation).
return true;
};
},
errors: false,
metaSchema: {
type: 'string',
additionalItems: false
}
});
And in the schemas:
{
...
"mobile": {
"type": "string",
"maxLength": 15,
"reformat": "numeric" // <—«
},
...
}
You can:
parentObject[propName] = format(data)
instead of using dataPath
Also in metaschema you can enum: Object.keys(reformat). Not sure why you need additionalItems: false, it only affects objects and you require that it is a string.
Thanks, I treated parentObject as if it's the root object.
I've updated accordingly.
ajv.addKeyword('reformat', {
type: 'string',
compile(sch, parentSchema) {
let format = reformat[sch];
return (data, dataPath, parentObject, propName, rootData) => {
parentObject[propName] = format(data);
return true;
};
},
errors: false,
metaSchema: {
type: 'string',
enum: Object.keys(reformat)
}
});
enum: Object.keys(reformat) helps but the error message is ambiguous: Fatal error: Error: keyword schema is invalid: data should be equal to one of the allowed values. Is there a way to modify this message to indicate which schema and give a pointer (to reformat keyword)?
I've improved this to both accept string or array (to apply multiple reformat methods in order).
let methodNames = Object.keys(reformat);
ajv.addKeyword('reformat', {
type: 'string',
compile(sch, parentSchema) {
// ensure array
let methods = !Array.isArray(sch) ? [sch] : sch;
return (data, dataPath, parentObject, propName, rootData) => {
// apply reformat methods in order
methods.forEach(method => {
data = reformat[method](data);
});
// update reformatted value
parentObject[propName] = data;
// always return true (for validation).
return true;
};
},
errors: false,
metaSchema: {
oneOf: [
{
type: 'string',
enum: methodNames
},
{
type: 'array',
items: {
type: 'string',
enum: methodNames
},
minItems: 1,
uniqueItems: true
}
]
}
});
For performance you can:
compile(sch, parentSchema) {
return Array.isArray(sch)
? (data, dataPath, parentObject, propName) => {
// ...
return true;
}
: (data, dataPath, parentObject, propName) => {
// ...
return true;
};
}
@onury I decided not to add the ability to modify data in custom format function, it would substantially complicate their code and also it would be an unnecessary duplication of functionality with custom keywords.
By the way, with custom keyword that modify data you must now use option modifying: true - see #392.