简体   繁体   中英

ajv - Ensure that given object structure matches structure of a custom type

I'm using ajv with TypeScript and have a custom type MyCustomType . When creating a validation schema I want to ensure that a specific property is of type MyCustomType . So ajv should validate its structure and decide whether this could be parsed to the given type.

I started with the following sample code ( and created a Codesandbox example for testing purposes )

import { AType } from "./AType"; // custom type
import { AnInterface } from "./AnInterface"; // custom interface
import Ajv from "ajv";

type MyComplexType = AType | AnInterface; // create a more complex type from the given imports

const ajvInstance = new Ajv();

const schema = {
  type: "object",
  properties: {
    myComplexType: { type: "string" } // value structure must match structure of  MyComplexType
  },
  required: ["myComplexType"],
  additionalProperties: false
};

const validate = ajvInstance.compile(schema);

const data = {
  myComplexType: {}
};

const isValid = validate(data);

if (isValid) {
  console.info("everything is fine");
} else {
  validate.errors?.forEach((error) => console.error(error.message));
}

Currently I don't know how to create a validation schema for the property myComplexType . I found some discussions

but I don't think these will help because

  • typeof just returns "object"
  • instanceof won't work for types

So do I have to create a custom keyword ( as described here ) and write my own validation logic ( inspect the object structure ) or are there any things I can already use? How should I configure myComplexType ?

I don't think you can create a custom type with this library, maybe you're confusing javascript types with typescript types. So you probably have 2 options: 1) instead of using types you use classes and use instanceof for validating an instance of your class, eg

class MyClass {}
const instanceofDef = require("ajv-keywords/dist/definitions/instanceof")
instanceofDef.CONSTRUCTORS.MyClass = MyClass
ajv.validate({instanceof: "MyClass"}, new MyClass())

or 2) you create a nested schema like this

const schema = {
  type: "object",
  properties: {
    myComplexType: { 
      type: "object",
      properties: {
        myProperty: { type: "string" },
        myFunction: { type: "function" },
        ...
      }
    }
  },
  required: ["myComplexType"],
  additionalProperties: false
};

const validate = ajvInstance.compile(schema);

const data = {
  myComplexType: {
    myProperty: "Hello World",
    myFunction: function(){}
  }
};

const isValid = validate(data);

I flattened your structure to be able to answer with one code segment. This is TypeScript, and runs on NODE JS successfully using Ajv.

The key is using the JSONSchemaType from Ajv to bride between Ajv and TypeScript. If the TYPE and AJV definitions dont match up, TypeScript will complain. This is great, especially if your editor has a TypeScript real-time checker built in (VS Code, VIM, ...)

Notice that the schema MyComplexTypeSchema is defined using the previously defined schemas. It does not have to be one huge nested schema. Have a schema for every TYPE and INTERFACE, and build your library from the ground up to support both TypeScript and Ajv.

'use strict'
import Ajv, {JSONSchemaType} from "ajv"
const ajvInstance = new Ajv();

type AType = "x" | "y";
const ATypeSchema : JSONSchemaType<AType> = {
  type:"string",
  enum:["x","y"]
}

interface AnInterface {
  prop: string;
}
const AnInterfaceSchema : JSONSchemaType<AnInterface> = {
  type:"object",
  properties:{
    prop:{type:"string"}
  },
  required:["prop"]
}

type MyComplexType = AType | AnInterface;
const MyComplexTypeSchema : JSONSchemaType<MyComplexType> = {
  oneOf:[ ATypeSchema,AnInterfaceSchema ]
}

const schema = {
  type: "object",
  properties: {
    myComplexType: MyComplexTypeSchema
  },
  required: ["myComplexType"],
  additionalProperties: false
};

const validate = ajvInstance.compile(schema);

const data1 = {
  myComplexType: {
    prop:"a string"
  }
};

var isValid = validate(data1);

if (isValid) {
  console.info("everything is fine in data1");
} else {
  validate.errors?.forEach((error) => console.error(error.message));
}

const data2 = {
  myComplexType: "x"
};

var isValid = validate(data2);

if (isValid) {
  console.info("everything is fine in data2");
} else {
  validate.errors?.forEach((error) => console.error(error.message));
}

const bad1 = {
  myComplexType: {}
};

var isValid = validate(bad1);

if (isValid) {
  console.info("everything is fine in bad1");
} else {
  validate.errors?.forEach((error) => console.error("bad1: "+error.message));
}


const bad2 = {
  myComplexType: "z"
};

var isValid = validate(bad2);

if (isValid) {
  console.info("everything is fine in bad2");
} else {
  validate.errors?.forEach((error) => console.error("bad2: "+error.message));
}

I left your "const schema" code alone, but it probably should be something like below, to be consistent with the other Type / Schema structuring.

type TopType = {
        myComplexType : MyComplexType
    }

const TopTypeSchema : JSONSchemaType<TopType> = {
  type: "object",
  properties: {
    myComplexType: MyComplexTypeSchema
  },
  required: ["myComplexType"],
  additionalProperties: false
};

const validate2 = ajvInstance.compile(TopTypeSchema);

// TypeScript will not let us build a BAD value if the type is defined
const data1:TopType = {
  myComplexType: {
    prop:"a string"
  }
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM