What version of Ajv you are you using?
6.2.1
What problem do you want to solve?
Make better use of TypeScript types with AJV.
We're using json-schema-to-typescript to generate typescript types from our JSON schemas. To use these, we're currently creating type guard functions that use the schema to validate a value as an unknown shape. If the schema validates, we know that the typescript type is accurate. Right now this is a manual process, because AJV doesn't provide a mechanism to tie a typescript type to a ValidateFunction when compiling a schema.
What do you think is the correct solution to problem?
Provide a way to compile<SomeType>(mySomeTypeSchema) which returns a ValidateFunction<T>, which might look something like (data: any,...) => data is T. Using this validate function in an if statement would then let TypeScript automatically cast data to T within the then branch without an explicit cast.
This might look something like
ValidateFunction to ValidateFunction<T=any> and the return type to data is Tcompile and other functions that return ValidateFunction to e.g. compile<T=any>(schema: object | boolean): ValidateFunction<T>;The complication is the return type of data is T | Thenable<any>. Since TypeScript doesn't know which you have statically, this doesn't get you all the way there. It does work if you remove the Thenable<any> return type option. Solving this will require either a more nuanced modelling of the types – perhaps dividing ValidateFunction from AsyncValidateFunction or providing a few overloads for ValidateFunction may solve it, though I admit I haven't looked that closely enough to know for sure.
Will you be able to implement it?
I'd be happy to help translate example use cases in to type signatures. Time to get deep enough familiarity with AJV is the main limiting factor to putting making the changes entirely myself.
If it's helpful (or anyone else is interested), here's what we're doing to work around this issue currently:
export function buildValidator<T>(ref: string): ValidateFunction<T> {
// we previously used `addSchema` to define `my-schema.json`
return ajv.compile({
$ref: `my-schema.json#/definitions/${ref}`
});
}
export interface ValidateFunction<T> extends Ajv.ValidateFunction {
_t?: T; // stop linter from complaining about unused T variable
}
function isValid<T>(
validator: ValidateFunction<T>,
candidate: any
): candidate is T {
return validator(candidate) === true;
}
const validateFoo = buildValidator<{foo: number}>("asdf");
const y: any = {};
if(isValid(validateFoo, y) {
y // is treated as a `{foo: number}` here.
}
Just to clarify, you want generated code to be typescript?
Probably not...
Hi @epoberezkin,
I'm not looking for AJV to generate types or for any change to the run-time behavior of AJV or even the existing TypeScript API. I'm hoping we can enhance AJV's built-in typescript types to enable users opt into better leveraging AJV as a way to generate type testing functions, which is ultimately what JSON schema is all about.
TypeScript supports a few features that are particularly relevant here:
data is T bit from my example compile type signature above)ValidationFunction<T> above)The two changes I suggested in my original message in this issue show an example of how that could be structured.
With those changes, I'd expect to be able to do the following:
const isPerson = ajv.compile<Person>("person.schema.json");
// isPerson is a type guard that proves it's argument is a Person.
// The return type of isPerson(data) is `data is Person`
if (isPerson(someData)) {
// TypeScript type checks `someData` as a `Person` in this block
alert(`${someData.firstName} ${someData.lastName}`);
} else {
// TypeScript knows that `someData` is _not_ a person in this block;
}
I believe these changes can be made a) without breaking the current TypeScript API, b) without changing any runtime behavior in AJV, and c) without adding additional machinery to e.g. generate types. This change is merely about better modeling the existing behavior of AJV with TypeScript's type system.
It's worth noting that there's nothing stopping users from inventing this machinery themselves. The workaround I posted in my follow-up message provides all of the same capabilities. However, this does seem like it should be a part of AJV, as the primary reason for using AJV is to check that data matches the set of contstraints for some type, so they can know they have a valid instance of that type. This is exactly what a type guard in TypeScript are for, so this seems like a more natural way to model validation functions in TypeScript.
Does this clarify the nature of the request? I'm happy to provide additional clarification as necessary.
Also, do you have concerns about making such a change? From my outside perspective, it seems like a win for users that is in the spirit of what AJV is already designed to do, but perhaps there are potential concerns I haven't considered.
Looks like it won’t hurt... cc @blakeembrey
@epoberezkin This should be good and all the parts were well documented above by @dcolthorp 👍 (just needs the generic param and default it to = any). I tend to have done this myself in the past by wrapping a function like const x = (x: any): x is Person => validate(x).
I had this exact idea yesterday, so I'm glad I checked the issues first and found this. I agree with @dcolthorp that this seems like something that should be able to be done with no runtime changes or additional layers. But so far, I've been a bit confounded by the existing return type of ValidationFunction. I haven't found a good way of making it act as a type guard when called one way or a Promise returning function in another.
Another complication is that, as I read this at least, type guards need to be on the function declaration proper, and not just on the type definition due to the fact that they're contextually typed. I'm wondering if this is just a matter of generating some JSDoc or commented type annotations in the compiled function? That would be a slight runtime change, but I don't think it would be noticeable.
Here's a TypeScript Playground link where I noodled around on some things. I did manage to get something kinda working with a combination of a default generic parameter and conditional types, but it's kind of complicated and may not be what we want at the end of the day.
Here's the final form of that experiment:
type Validator4<T=void> =
T extends void ? (data: any) => boolean :
T extends Promise<any> ? (data: any) => T :
(data: any) => data is T;
// Example guard functions
const isStringBool = <Validator4>((data: any) => typeof data === "string");
const isString = <Validator4<string>>((data: any) => typeof data === "string");
const eventuallyValidate = <Validator4<Promise<string>>>
((data: any) => typeof data === "string" ? Promise.resolve(data) : Promise.reject())
Something like this _might_ work for ValidateFunction, but in trying to figure out how to get it to check, I may have strayed a bit from how people actually use AJV.
A simpler interface would probably be easier to type check, so I might be leaning toward a smaller wrapper for TypeScript users with fewer union return types, either as an alternate interface or maybe a separate package.
But either way, having the compiled code generate an annotation (so that it could be more easily imported) would probably be a good idea (although that's probably a different ticket).
I've been working on https://github.com/ostrowr/ts-json-validator the last couple days, which does pretty much exactly this, and happens to use ajv behind the scenes anyway. If a maintainer would be interested, I'm happy to port a bunch of that work over to ajv, or implement a plugin that implements these types.
@ostrowr your library looks great and the clear, concise documentation is refreshing. thank you thank you thank you. (now to give it a try...)
@ostrowr implementing plugin is better as Ajv is not a typescript library.
@ostrowr Do you think it is possible to implement this functionality, solely by updating the types? Or does it depend on updates to the Ajv runtime as well?
@ostrowr Do you think it is possible to implement this functionality, solely by updating the types? Or does it depend on updates to the Ajv runtime as well?
@Janpot Unfortunately, as it currently stands it would probably require some changes to the runtime, since each sub-schema (currently) needs to be marked using "createSchema."
@dcolthorp mere 2 years later consider to include it in upcoming v7 release :)
@ostrowr now would be the great time to get some of the ideas from ts-json-validator into ajv - it's now re-written into typescript (v7-alpha branch). I plan to release v7 beta end of September, it would be great if you could have a look. Thank you.
I've never seen that many thumbs-up on a single issue in Ajv - thanks a lot for supporting this feature!
Validating type guards are now in v7-alpha branch, coming to npm soon.
I've also added JSONSchemaType<T> - a utility type that for a given data interface recursively constructs the type for JSON Schema for this data - it supports records, dictionaries, arrays and tuples (of any depth).
So if you write JSON schemas manually rather than generate them from interfaces, e.g. when typescript interface is not expressive enough to define dependencies or when you want to add any additional restrictions on top of the interface, this type could simplify it and help avoiding mistakes and ensuring correctness of the schema.
Check it out here:
type: https://github.com/ajv-validator/ajv/blob/v7-alpha/lib/types/json-schema.ts
tests with the usage example: https://github.com/ajv-validator/ajv/blob/v7-alpha/spec/types/json-schema.spec.ts
Any feedback or suggestions would be really appreciated :)
@ostrowr now would be the great time to get some of the ideas from ts-json-validator into ajv - it's now re-written into typescript (v7-alpha branch). I plan to release v7 beta end of September, it would be great if you could have a look. Thank you.
I probably won't have time to make any serious contributions before the end of September, but I'll certainly take a look and see where I can help ASAP!
Most helpful comment
Hi @epoberezkin,
I'm not looking for AJV to generate types or for any change to the run-time behavior of AJV or even the existing TypeScript API. I'm hoping we can enhance AJV's built-in typescript types to enable users opt into better leveraging AJV as a way to generate type testing functions, which is ultimately what JSON schema is all about.
TypeScript supports a few features that are particularly relevant here:
data is Tbit from my examplecompiletype signature above)ValidationFunction<T>above)The two changes I suggested in my original message in this issue show an example of how that could be structured.
With those changes, I'd expect to be able to do the following:
I believe these changes can be made a) without breaking the current TypeScript API, b) without changing any runtime behavior in AJV, and c) without adding additional machinery to e.g. generate types. This change is merely about better modeling the existing behavior of AJV with TypeScript's type system.
It's worth noting that there's nothing stopping users from inventing this machinery themselves. The workaround I posted in my follow-up message provides all of the same capabilities. However, this does seem like it should be a part of AJV, as the primary reason for using AJV is to check that data matches the set of contstraints for some type, so they can know they have a valid instance of that type. This is exactly what a type guard in TypeScript are for, so this seems like a more natural way to model validation functions in TypeScript.
Does this clarify the nature of the request? I'm happy to provide additional clarification as necessary.
Also, do you have concerns about making such a change? From my outside perspective, it seems like a win for users that is in the spirit of what AJV is already designed to do, but perhaps there are potential concerns I haven't considered.