23

I have an object which contains one or more properties of type date. I would like to validate the object using the ajv json schema validator package. I could convert the properties of type date to a string by using the toISOString(). But the object can be quiet big and thus I dont want to convert all the date-properties of the whole object. Is there a solution other than converting the date to a string? Could I somehow create a custom ajv schema validator?

 // My example schema
const schema = {
  "properties": {
    "createdAt": { 
       "type": "string",
       "format": "date-time"
    },
       "lastName": { "type": "string" },
       "firstName": { "type": "string" }
  }
};

// My example testobject
const testObj = {
   createdAt: new Date(),
   lastName: "Doe",
   firstName: "John"
}

// The validation
const validate = ajv.compile(schema);
const valid = validate(testObj);
if(!valid) console.log('Invalid: ' + ajv.errorsText(validate.errors));

This will do a console log, because the testObj.createdAt is a date and not a string.

A. Bolleter
  • 231
  • 1
  • 2
  • 4
  • Take a look at this plugin https://github.com/epoberezkin/ajv-keywords is related what you are looking for I think – leocoder Sep 10 '19 at 20:21
  • 1
    Simply change your ajv schema from "type": "string" to "type": "object" and the built-in ajv "date-time" format will work. Tested in ajv version 6.10.2. – Ananda Masri Nov 20 '19 at 21:01
  • 2
    Because the Date object is bigger (and less portable) than a date/time string, I'd actually recommend converting your Date objects to strings - especially if you're planning to send the validated data to your back-end server for re-validation. Not all back-end server platforms would easily validate a javascript Date object. – Ananda Masri Nov 20 '19 at 21:13
  • 1
    @leocoder how exactly would the package you suggested solve the problem? The ajv package already allows to define custom ajv validators. – Ananda Masri Nov 20 '19 at 21:23

4 Answers4

7

Simply change your ajv schema from "type": "string" to "type": "object" and the built-in ajv date-time format will work. Here's a working fiddle.

You can also define a custom ajv format to validate a Date object (or string) like this:

ajv = new Ajv();

ajv.addFormat('custom-date-time', function(dateTimeString) {
  if (typeof dateTimeString === 'object') {
    dateTimeString = dateTimeString.toISOString();
  }

  return !isNaN(Date.parse(dateTimeString));  // any test that returns true/false 
});

... and invoke your custom format in the ajv schema like this:

const schema = {
  "properties": {
    "createdAt": {
      "type": "object",
      "format": "custom-date-time"

Putting it all together here's the code and a working fiddle for creating a custom date format:

// My example schema

const schema = {
  "properties": {
    "createdAt": {
      "type": "object",
      "format": "custom-date-time"
    },
    "lastName": {
      "type": "string"
    },
    "firstName": {
      "type": "string"
    },
    "required": [ 'createdAt', 'lastName', 'firstName' ],
    "additionalProperties": false,
  }
};

// My example testobject
const testObj = {
  createdAt: new Date(),
  lastName: "Doe",
  firstName: "John"
}


// The validation
ajv = new Ajv();

ajv.addFormat('custom-date-time', function(dateTimeString) {
  if (typeof dateTimeString === 'object') {
    dateTimeString = dateTimeString.toISOString();
  }

  return !isNaN(Date.parse(dateTimeString));  // return true/false 
});

const validate = ajv.compile(schema);
const valid = validate(testObj);

if (valid)
  alert('valid');
else
  alert('Invalid: ' + ajv.errorsText(validate.errors));
Ananda Masri
  • 363
  • 2
  • 8
  • 4
    Test passed with {"date": 1}, so not a valid answer. – Polv May 16 '20 at 23:51
  • @Polv, the OP asked "Can I validate a date using ajv json schema, without converting the date to string?". I've updated my answer to address your specific validation requirement by including "required" & "additionalProperties" in the validation schema. – Ananda Masri Aug 03 '20 at 00:10
  • This is an active issue with upgrading to objection 3.x.x. Many answers on github issues force to you store the value as string instead of a date. Thank you – jboxxx Jun 12 '22 at 21:18
  • 1
    @AnandaMasri I tried this and it does not work quite as expected. AJV ignores `format` when the `type` is "object". You can try this in your fiddle by changing the test object date to `new Date('rubbish')`; it incorrectly marks this as valid. I was able to successfully add a keyword to do the job. Will post in an answer. – Andrew Eddie Aug 25 '22 at 05:59
0

One of the valid way is to convert first to JSON Schema-compatible object.

function makeJsonSchemaCompatible (obj) {
  if (Array.isArray(obj)) {
    return obj.map((subObj) => makeJsonSchemaCompatible(subObj))
  } else if (obj && typeof obj === 'object') {
    const replacement = {}
    const className = obj.constructor.name
    if (className !== 'Object') {
      replacement.__className = className
    }
    Object.entries(obj).map(([k, v]) => { replacement[k] = makeJsonSchemaCompatible(v) })
    return replacement
  }

  return obj
}
Polv
  • 1,918
  • 1
  • 20
  • 31
  • How is this supposed to work? As far as I can tell `makeJsonSchemaCompatible(new Date())` returns only `{ __className: "Date" }`, i.e., all the actual data of the `Date` is lost. Also, `JSON.parse(JSON.stringify(new Date()))` does not round-trip properly. The type changes from `Date` to string. – bluenote10 May 22 '21 at 16:04
  • The idea is there is reviver and replacer; but I will need to fix my code. – Polv May 22 '21 at 18:18
0

Unfortunately the format property does not proc' for object types, so custom formats aren't an option. However, I was able to add a custom keyword (inspired by looking at the instance keyword, which actually gets you half way there) that gave me the desired results (that being the value must be a Date object and the Date must be valid).

const { equal } = require('assert');
const Ajv = require('ajv');
const { _ } = require('ajv');
const ajv = new Ajv();
const schema = {
  type: 'object',
  properties: {
    jsdate: {
      type: 'object',
      isDate: true,
    },
  },
};

ajv.addKeyword({
  keyword: 'isDate',
  type: 'object',
  code(ctx) {
    const { data } = ctx;
    ctx.pass(_`${data} instanceof Date && !isNaN(+${data})`);
  },
});

const validate = ajv.compile(schema);

equal(validate({ jsdate: new Date() }), true, 'should succeed');

equal(validate({ jsdate: {} }), false, 'should fail for random object');

equal(
  validate({ jsdate: '2001-01-01' }),
  false,
  'should fail for valid date string'
);

equal(
  validate({ jsdate: new Date('rubbish') }),
  false,
  'should fail if Date is invalid'
);
Andrew Eddie
  • 988
  • 6
  • 15
0

It seems that you could achieve the expected result by using the instanceof keyword (part of ajv-keywords) : .

const Ajv = require("ajv");
const addKeywords = require("ajv-keywords");

const ajv = new Ajv(); // options can be passed, e.g. {allErrors: true}
addKeywords(ajv);

// My example schema
const schema = {
  type: "object",
  properties: {
    createdAt: {
      instanceof: "Date",
    },
    lastName: { type: "string" },
    firstName: { type: "string" },
  },
};

// My example testobject
const testObj = {
  createdAt: new Date(),
  lastName: "Doe",
  firstName: "John",
};

// The validation
const validate = ajv.compile(schema);
const valid = validate(testObj);
if (!valid) console.log("Invalid: " + ajv.errorsText(validate.errors));