ajv-validator / ajv

The fastest JSON schema Validator. Supports JSON Schema draft-04/06/07/2019-09/2020-12 and JSON Type Definition (RFC8927)
https://ajv.js.org
MIT License
13.76k stars 872 forks source link

Creating a custom type #147

Closed tmerlet closed 8 years ago

tmerlet commented 8 years ago

Hey,

I was wondering if I can create custom types. I've looked at "Defining custom keywords" but this doesn't seem to fit. For example a type 'buffer' or 'react element'.

Thanks

epoberezkin commented 8 years ago

You can use custom keywords to validate custom types. From the point of view of the schema it will still be an object, but the keyword can test that it is instanceof some class and do some other stuff with it depending on the value of the keyword (i.e. its schema).

tmerlet commented 8 years ago

Thanks for your answer. sorry I'm still a little bit confused how to achieve this. this is my current thinking, should this work?

ajv.addKeyword('type', { validate: function (schema, data) {
  return myCustomTypeValidation(data)
}, errors: false });

How do I make sure that other types return their respective errors?

epoberezkin commented 8 years ago

no, you cannot extend or override type keyword. But you can define a new keyword that would do validation for some additional "type" (it's not a type really in JS, the type is 'object' in most cases).

ajv.addKeyword('buffer', { compile: function(schema) {
  return function(data) {
    if (data instanceof Buffer) {
      // do some schema based validation, e.g. buffer size
    } else {
      return false;
    }
  };
} });

So if the data is not instance of Buffer the validation will fail.

Or you can even add a custom keyword instanceof that would accept class constructor name as a string:

var CLASSES = {
  Buffer: Buffer,
  RegExp: RegExp,
  // ...
};

ajv.addKeyword('instanceof', { compile: function(schema) {
  var Class = CLASSES[schema];
  return function(data) {
    return data instanceof Class;
  };
} });

var schema = {
  type: 'object',
  properties: {
    buf: { instanceof: 'Buffer' }
  }
};

// ...

As long as your schema is serialisable it will all work (so you cannot use constructor function as the value of the keyword). Data itself does not have to be serialisable, so you can use and validate any instances in the data.

tmerlet commented 8 years ago

Thanks @epoberezkin, that really helped :+1:

mbroadst commented 8 years ago

Sorry to hijack the issue, but I think this is where my question belongs!

I still think there is a use case for custom types themselves. In my case, I'm writing some validation schemas for RethinkDB entries, and they have a custom supported type "point" which represents a geo location. Understandably I could create a custom keyword and do something like:

{
   type: 'object',
   properties: {
     thing: { point: true },
     or: { instanceOf: 'point' }
  }
}

but I feel like that will be quite unintuitive to the user who would expect a point to be a type just like everything else:

{
   type: 'object',
   properties: {
     thing: { type: 'point' }
  }
}

Is there no room for adding this kind of functionality? I'm sure I've missed some crucial detail, but if it were possible it would greatly improve usability/readability imho

mbroadst commented 8 years ago

oh hmm, it looks like just creating a schema for that and doing addSchema on the main ajv instance might suffice?

Got it from reading through other issues:

let PointSchema = {
  id: 'point',
  type: 'object',
  properties: {
    latitude: { type: 'number' },
    longitude: { type: 'number' }
  },
  required: [ 'latitude', 'longitude' ]
};

let PersonSchema = {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1, default: 'name' },
    location: { $ref: 'point' }
  },
  required: [ 'name', 'location' ],
  additionalProperties: false
};

sorry for the noise, hth someone in the future 😄

epoberezkin commented 8 years ago

Yes, that's what ref is for

mbroadst commented 8 years ago

@epoberezkin as a follow up question to that, is it possible in the above example to have the point schema accept either { latitude: 100, longitude: 200 } or [100, 200] (geojson format)? I've been wrestling a bit with anyOf can't seem to work it out

maybe I should turn this issue into a wiki for solutions :)

{
  id: 'point',
  anyOf: [
    {
      type: 'object',
      properties: {
        latitude: { type: 'number' },
        longitude: { type: 'number' }
      },
      required: [ 'latitude', 'longitude' ]
    },
    {
      type: 'array',
      items: { type: 'number' }
    }
  ]
}
webarthur commented 5 years ago

I just made a module to simplify some AJV issues. It also includes a new function called .addType():

Github: https://github.com/webarthur/super-ajv

NPM: https://www.npmjs.com/package/super-ajv

const ajv = new Ajv()

ajv.addType('mongoid', {
  compile: function () {
    return function (data) {
      const re = /^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i
      return re.test(data)
    }
  }
})

const schema = {
  properties: {
    user_id: { type: 'mongoid' }
  }
}

You can also:

const schema = {
  properties: {
    '*name': 'string',
    '*email': 'email',
    'age': 'number',
    '*message': 'string',
  }
}

Enjoy!

KrishnaPG commented 5 years ago

@webarthur The super-ajv looks interesting.

Extending the base json-schema meta schema is required sometimes, since it is not always possible to #ref the types. For example,

"type": "service",
"interface": {
    "divide": {
        "description":"Divide one number by another",
        "type":"method",
        "returns": { "type": "number" },
        "params": {
            "dividend": {"type":"number","name":"dividend"},
            "divisor" : {"type":"number","name":"divisor"}
        }
        "requiredParams": ["dividend", "divisor"],
        "additionalParams": false
    },
}

Json-schema is too much focused on defining data and hence too restrictive to define interfaces. Extending its meta schema allows it to cover more ground.

burawi commented 5 years ago

I just made a module to simplify some AJV issues. It also includes a new function called .addType():

Github: https://github.com/webarthur/super-ajv

NPM: https://www.npmjs.com/package/super-ajv

const ajv = new Ajv()

ajv.addType('mongoid', {
  compile: function () {
    return function (data) {
      const re = /^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i
      return re.test(data)
    }
  }
})

const schema = {
  properties: {
    user_id: { type: 'mongoid' }
  }
}

You can also:

const schema = {
  properties: {
    '*name': 'string',
    '*email': 'email',
    'age': 'number',
    '*message': 'string',
  }
}

Enjoy!

Why not requesting a merge ?

thesayyn commented 5 years ago

+1 for this issue. Ability to defining new data types would be awesome.

Vadorequest commented 4 years ago

Is this not possible? I'm trying to check if a variable is a function.

https://stackoverflow.com/questions/60196587/ajv-check-property-is-a-function

hanshamm commented 4 years ago

I also would like to have the ability to add custom formats. For example, if I want to create a format for International Bank Account Number validation (IBAN):

var IBAN = require('iban');
const schema= {
    "format": "iban"
};

ajv.addFormat('iban', {
  compile: function () {
    return function (data) {
      return IBAN.isValid(data);
    }
  }
});

otherwise I would have to do it like this:

var IBAN = require('iban');

ajv.addKeyword('ibanValidator', {
    type: 'string', 
    compile: function (sch, parentSchema) {
        if (sch === true) {
            return function (data) {
                return IBAN.isValid(data);
            }
        }
        return true;
    }
  });

const schema= {
    "ibanValidator": true
};

you see, it would be much cleaner code and better usability if having custom types.

epoberezkin commented 4 years ago

@hanshamm Custom formats are already supported - see the docs

TriupLauro commented 2 years ago

I just made a module to simplify some AJV issues. It also includes a new function called .addType():

Github: https://github.com/webarthur/super-ajv

NPM: https://www.npmjs.com/package/super-ajv

const ajv = new Ajv()

ajv.addType('mongoid', {
  compile: function () {
    return function (data) {
      const re = /^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i
      return re.test(data)
    }
  }
})

const schema = {
  properties: {
    user_id: { type: 'mongoid' }
  }
}

You can also:

const schema = {
  properties: {
    '*name': 'string',
    '*email': 'email',
    'age': 'number',
    '*message': 'string',
  }
}

Enjoy!

I tried to use it but couldn't get it to work.

Am I missing somethings on how it is supposed to be imported, and how it can be used in schema and compiled ?

webarthur commented 2 years ago

Hi @TriupLauro! Can you create this issue in super-ajv with your code?

TriupLauro commented 2 years ago

Hi @TriupLauro! Can you create this issue in super-ajv with your code?

Sorry I deleted my code and used another way to achieve my goal. But maybe I can post what the code did look like.