Ajv: Pass data path, parent data and property name to format validation function

Created on 4 Nov 2016  Â·  11Comments  Â·  Source: ajv-validator/ajv

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.

  • For example, you can accept a phone number with spaces and some limited punctuation chars but after you validate the value, you'd remove all non-numeric chars and insert the value in db.
  • Another real-life example, sometimes users input their names, all lower-cased. It is still valid data but just not in proper format. You could re-format it to title-case with this feature.

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.

enhancement wontfix

All 11 comments

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.

Was this page helpful?
0 / 5 - 0 ratings