question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Integration strategy with ajv library for parsing "Date" fields

See original GitHub issue

This issue is more about the use of the ajv library rather than typescript-json-schema, but I figured it would be appropriate to ask here, due to the specific interaction with typescript-json-schema.

For the following interface, typescript-json-schema creates a perfect schema:

export interface Breakfast {
    location: string
    scheduled: Date;
}
{
    "type": "object",
    "properties": {
        "location": {
            "type": "string"
        },
        "scheduled": {
            "description": "Enables basic storage and retrieval of dates and times.",
            "type": "string",
            "format": "date-time"
        }
    },
    "required": [
        "location",
        "scheduled"
    ],
    "$schema": "http://json-schema.org/draft-04/schema#"
}

Notice that the field “scheduled” of type “Date” gets serialized as a “string” (with format “date-time”).

This works perfectly when we JSON.stringify a Breakfast object. But when we try to JSON.parse it back in, it will pass schema validation via ajv, but we will actually end up with our “scheduled” field set as a string rather than a Date.

Does anyone have any suggestions for how to end up with with a genuine “Date” object for all Date fields?

ajv is able to apply modifications to objects as it parses them. Here is a discussion titled “Custom coerce types/functions” where there are some leads on how this could work: https://github.com/epoberezkin/ajv/issues/141. I tried the approach described there but was unable to get it working. I think the way to go is to have typescript-json-schema write a custom keyword for all “Date” fields, and then figure out how to have ajv recognize this keyword and convert the strings to Dates.

But I might be missing something very obvious. I would be very happy to hear how others are dealing with this issue

Issue Analytics

  • State:open
  • Created 7 years ago
  • Reactions:7
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

15reactions
benny-medflytcommented, Dec 6, 2017

Here is a system I found that works pretty well. It uses a JSON schema custom keyword.

The following example will use the “Date” type, but this system can be used for any other custom type or type from a library.

1. Create type aliases

For each non-primitive type you want to use, create a type alias in the following way:

CustomTypes.ts

/**
 * @TJS-type string
 * @xreviver JsonDate
 */
export interface JsonDate extends Date {}

(We use TypeScript interface instead of type, since “typescript-json-schema” won’t work properly with type)

2. Use the type alias in your interface

import { JsonDate } from "./CustomTypes";

export interface Breakfast {
    location: string;
    scheduled: JsonDate;
}

3. Generate your JSON Schema

Make sure you are using a recent version of “typescript-json-schema”. I am using version 0.20.0.

Use “typescript-json-schema” as usual to generate your JSON schema, but use the validationKeywords option to add the “xreviver” keyword (note that it was used above):

const generator = TJS.buildGenerator(program, {
    required: true, // This is optional but I personally think it should always be set
    validationKeywords: ["xreviver"]
    // Continue using any other options you are using here... (I personally don't use any other options)
});

If instead of the API, you are using the “typescript-json-schema” command line program, then use the --validationKeywords option (giving it the value “xreviver”)

If you are curious, scroll down to the end to see the resulting JSON schema.

4. Convert your data structure to JSON

Nothing special needs to be done here:

const breakfast: Breakfast = {
    location: "kitchen",
    scheduled: new Date()
};
console.log(JSON.stringify(breakfast));
// Prints:
// '{"location":"kitchen","scheduled":"2017-12-06T17:53:50.448Z"}'

Note: Our system assumes that your custom types all have a toJSON() method that will output a string. This holds for the regular Date object, but make sure it also holds for other types you plan to use.

5. Create your “reviver” functions

export type Reviver = (val: string) => any;

function dateParser(val: string): Date {
    const date = new Date(val);
    if (isNaN(<any>date)) {
        throw new Error(`Invalid date: ${val}`);
    }
    return date;
}

export const revivers: { [key: string]: Reviver } = {
    "JsonDate": dateParser
};

Note that the key in the reviver dictionary (“JsonDate”) matches the string we wrote above in CustomTypes.ts (@xreviver JsonDate).

If you have additional types, then use different “@xreviver” values, and add a parser for each one to the “revivers” dictionary. Each custom type you use should have a unique “@xreviver” value.

Note that as you can see in the “JsonDate” parser above, your parser functions should “throw new Error(…)” if there is a parse error.

6. Get ajv ready

Make sure you are using a recent version of ajv, since older versions will not work correctly for us. I am using version 5.5.1.

Note that there is currently an annoying incompatibility between “typescript-json-schema” and versions of “ajv” >= 5.0.0. So make sure to follow the instructions in these ajv release notes (The section: “If you need to continue using draft-04 schemas”)

Once you have your “ajv” object we need to tell it about our custom keyword:

import * as Ajv from "ajv";

const ajv = new Ajv({ /* ... */});
// ...

const validateFn: Ajv.ValidateFunction = (keywordValue: string, data: any, _parentSchema?: object, dataPath?: string, parentData?: object | Array<any>, parentDataProperty?: string | number, _rootData?: object | Array<any>): boolean => {
    const setErrorMessage = (msg: string): void => {
        validateFn.errors = [{
            keyword: "xreviver",
            dataPath: "" + dataPath,
            schemaPath: "", // This field appears to be ignored
            params: {
                keyword: "xreviver"
            },
            message: msg,
            data: data
        }];
    };

    if (typeof data === "string") {
        const reviverFn: Reviver | undefined = <any>(revivers[keywordValue]);
        if (reviverFn === undefined) {
            setErrorMessage(`Unknown xreviver function: "${keywordValue}"`);
            return false;
        }

        let parsed: any;
        try {
            parsed = reviverFn(data);
        } catch (e) {
            // Take only the first line, because the rest may contain junk (a stack trace)
            const parseError = e.message.split("\n")[0];

            setErrorMessage(`${keywordValue}: ${parseError}`);
            return false;
        }
        if (parentData !== undefined && parentDataProperty !== undefined) {
            (<any>parentData)[parentDataProperty] = parsed;
        }
    }
    return true;
};

ajv.addKeyword("xreviver", {
    modifying: true,
    validate: validateFn
});

The above code should be used exactly as is. (But just in case you need it, here is the reference for ajv custom validation: https://github.com/epoberezkin/ajv/blob/master/CUSTOM.md#define-keyword-with-validation-function)

7. Validate and “Revive” your JSON

Now use ajv normally:

const schema = /* Load my JSON schema that was created in step 3 */
const validate = ajv.compile(schema);

// Load our saved JSON data:
const data = JSON.parse('{"location":"kitchen","scheduled":"2017-12-06T17:53:50.448Z"}');

// The following, will not only validate "data", but will also modify it!
if (!validate(data)) {
    throw new Error(`Invalid input: ${JSON.stringify(validate.errors)}`);
}

console.log(data);
// Prints:
// { location: 'kitchen', scheduled: 2017-12-06T17:53:50.448Z }

// Note that the field "data.scheduled" was converted from a string to a Date object:
console.log(data.scheduled.getFullYear());
// 2017

The End and Good Luck!

Bonus

If you are curious, here is the JSON Schema that is generated. Notice that the “scheduled” field has a ref to “JsonDate”, which has type string, and our custom “xreviver” keyword attached:

{
  "type": "object",
  "properties": {
    "location": {
      "type": "string"
    },
    "scheduled": {
      "$ref": "#/definitions/JsonDate"
    }
  },
  "required": [
    "location",
    "scheduled"
  ],
  "definitions": {
    "JsonDate": {
      "type": "string",
      "xreviver": "JsonDate"
    }
  },
  "$schema": "http://json-schema.org/draft-04/schema#"
}
2reactions
transitive-bullshitcommented, Aug 27, 2019

For anyone ending up here, I’ve updated @benny-medflyt’s excellent answer for changes to ajv and added unit tests. This version supports Date and Buffer encoding and decoding, including a concrete usage example with typescript-json-schema.

Check out the source and tests for example usage.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Custom coerce types/functions #141 - ajv-validator/ajv - GitHub
I'm transforming the swagger schema into an ajv schema, so transforming ... Integration strategy with ajv library for parsing "Date" fields ...
Read more >
Using with TypeScript - Ajv JSON schema validator
The fastest JSON schema Validator. Supports JSON Schema draft-04/06/07/2019-09/2020-12 and JSON Type Definition (RFC8927)
Read more >
How to handle request validation in your Express API
First things first, parse that JSON request body · Integrate Ajv (Another JSON Schema Validator) into your application · Using a JSON schema...
Read more >
Using AJV for schema validation with NodeJS - Medium
Ajv does not format our error messages. We can directly access ErrorObject and do some parsing instead, or use another library such as...
Read more >
CX Contact - Genesys Documentation
When it comes to your outbound strategy, omnichannel engagement is critical because highly personalized, timely and relevant notifications ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found