ajv-validator/ajv

Register custom validator without modifying the input schema

mikestead opened this issue · 1 comments

I've done a little searching but couldn't spot this raised or covered anywhere.

What version of Ajv you are you using?

8.16.0

What problem do you want to solve?

I'd like to add a custom validator to an existing schema without modifying the definition which I don't control. This would augment any validation already present on that schema.

What do you think is the correct solution to problem?

Something similar to avj.addKeyword(...) but instead add a validator targeting a specific schema path that's been registered with the avj instance.

avj.compile(jsonSchemaDefinitions);
avj.addValidator({
  // firstName having an inline schema
  schemaPath: '#/definitions/Account/properties/firstName',
  validate: (schema, data) => {
    // custom validation here
    return true;
  },
  errors: false,
})

const validate = avj.getSchema('#/definitions/Account');
validate({ firstName: "james" });

Will you be able to implement it?

No bandwidth currently

I've created a utility to work around this via keywords so I'll close the issue.

For anyone interested...

const avj = new AVJ({ allErrors: true });

addSchemaValidator(avj, {
  schemaDefs: jsonSchemaDefinitions,
  schemaPath: '#/definitions/Account/properties/firstName,
  keyword: 'x-accountNameVerifier',
  validate: ({ data: firstName, keyword, errors }) => {
    if (!isValidName(firstName)) {
      errors.push({
        keyword,
        message: 'must pass a valid firstName',
      });
      return false;
    }
    return true;
  },
  errors: true,
});

avj.compile(jsonSchemaDefinitions);

const validateAccount = avj.getSchema('#/definitions/Account');

if (!validateAccount({ firstName: "james" })) {
  console.log(validateAccount.errors)
}
import AVJ, { AnySchemaObject, FuncKeywordDefinition, SchemaValidateFunction, ErrorObject  } from 'ajv';
import { DataValidationCxt } from 'ajv/dist/types';

export type ValidateParams = {
  schema: any;
  data: any;
  parentSchema?: AnySchemaObject;
  dataCxt?: DataValidationCxt;
  errors: Partial<ErrorObject>[];
};

export type SchemaValidator = (params: ValidateParams) => boolean;

export type SchemaValidatorDefinition = {
  schemaDefs: any;
  schemaPath: string;
  keyword: string;
  validate: SchemaValidator;
} & Omit<FuncKeywordDefinition, 'keyword' | 'validate'>;

export function addSchemaValidator(avj: AVJ, validatorDef: SchemaValidatorDefinition): AVJ {
  const { schemaDefs, schemaPath, validate, ...keywordDef } = validatorDef;

  let path = schemaPath;
  if (path.startsWith('#')) path = path.slice(1);
  if (path.startsWith('/')) path = path.slice(1);

  let defs = schemaDefs;
  for (const segment of path.split('/')) {
    defs = defs[segment];
  }

  const keyword = keywordDef.keyword;
  defs[keyword] = true;

  const validateWrapperRef: SchemaValidateFunction = validateWrapper;

  function validateWrapper(schema: any, data: any, parentSchema?: AnySchemaObject, dataCxt?: DataValidationCxt) {
    const errors: Partial<ErrorObject>[] = [];
    const isValid = validate({
      schema,
      data,
      parentSchema,
      dataCxt,
      keyword,
      errors,
    });
    if (!isValid && errors.length) {
      validateWrapperRef.errors = errors;
    }
    return isValid;
  }

  avj.addKeyword({ ...keywordDef, validate: validateWrapper });

  return avj;
}