Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: type-aware validation functions #736

Closed
dcolthorp opened this issue Mar 15, 2018 · 16 comments
Closed

Feature request: type-aware validation functions #736

dcolthorp opened this issue Mar 15, 2018 · 16 comments

Comments

@dcolthorp
Copy link

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

  1. Change ValidateFunction to ValidateFunction<T=any> and the return type to data is T
  2. Change compile 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.

@dcolthorp
Copy link
Author

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.
}

@epoberezkin
Copy link
Member

Just to clarify, you want generated code to be typescript?

@epoberezkin
Copy link
Member

Probably not...

@dcolthorp
Copy link
Author

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:

  1. Type Guards enable users to encode in the type of a function that it is a predicate that proves a value is a valid instance of some type. I'd like a way to invoke AJV which returns a validation function which is a type guard for a type which I provide. (The data is T bit from my example compile type signature above)
  2. A generic type definition for AJV would let users pass in a TypeScript type that corresponds to their schema at the time the schema is compiled, and TypeScript would henceforth know that that particular AJV validation function is a validator for the specified type. (ValidationFunction<T> above)
  3. A generic parameter default is one way we might be able to introduce this change without breaking the API for users who are not providing a TypeScript type. Another option may be providing an overload.

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.

@epoberezkin
Copy link
Member

Looks like it won’t hurt... cc @blakeembrey

@blakeembrey
Copy link
Collaborator

@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).

@AndrewO
Copy link

AndrewO commented Sep 27, 2018

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).

@ostrowr
Copy link

ostrowr commented Nov 3, 2019

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.

@cogell
Copy link

cogell commented Dec 4, 2019

@ostrowr your library looks great and the clear, concise documentation is refreshing. thank you thank you thank you. (now to give it a try...)

@epoberezkin
Copy link
Member

@ostrowr implementing plugin is better as Ajv is not a typescript library.

@Janpot
Copy link

Janpot commented Feb 28, 2020

@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
Copy link

ostrowr commented Mar 2, 2020

@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."

@epoberezkin
Copy link
Member

@dcolthorp mere 2 years later consider to include it in upcoming v7 release :)

@epoberezkin
Copy link
Member

@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.

@epoberezkin epoberezkin added this to the 7.0.0-beta milestone Sep 14, 2020
@epoberezkin
Copy link
Member

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
Copy link

ostrowr commented Sep 14, 2020

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

7 participants