sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.77k stars 152 forks source link

`Check` doesn't validate strings with enums #940

Closed dswbx closed 1 month ago

dswbx commented 1 month ago

If I use Type.String({ enum: [...] }) the Check function only checks if the value given is a string. Here is an example comparing it to ajv and @cfworker/json-schema:

import { Type } from '@sinclair/typebox';
import { Check } from '@sinclair/typebox/value';
import { Validator } from '@cfworker/json-schema';
import Ajv from 'ajv';

const schema = Type.Object({
  type: Type.String({ enum: ['one', 'two'] }),
});

const obj = { type: 'three' };

console.log('Typebox', Check(schema, obj));
// Typebox true

console.log('@cfworker/json-schema', new Validator(schema).validate(obj));
// @cfworker/json-schema {valid: false, errors: Array[2]}

const ajv_validate = new Ajv().compile(schema);
const ajv_result = ajv_validate(obj);
console.log('ajv', ajv_result, ajv_validate.errors);
// ajv false [{…}]

I've read in other issues, that I could use a Union with Literal's, but this would mess up my schema (and want to keep it readable). Is there a reason why enums for String aren't checked (like limitation, or just no completed yet)? Alternatively, are there ways I could still use Check, like can I add custom validations?

(I can't use AJV because I'm using it inside a cloudflare worker, and while @cfworker/json-schema works good, I'd still prefer to use a single library).

Thanks in advance!

sinclairzx81 commented 1 month ago

@dswbx Hi,

By default, the TypeBox compiler and value checkers will only validate against structures defined by TypeBox (so it only checks a subset of the specification). This may change in later revisions where there is some consideration going towards developing a full standards compliant validator that supports all keywords, but this is a fair way off.

In the interim, you can register custom schematics + inference in the following way.

import { Type, Kind, SchemaOptions, TypeRegistry, Static } from '@sinclair/typebox'
import { Value } from 'src/value/value'

// create a custom type on the type registry 
TypeRegistry.Set('StringEnum', (schema: { enum: string[] }, value: unknown) => {
  return typeof value === 'string' && schema.enum.includes(value)
})

// create a type method for string enum
export function StringEnum<T extends string[]>(values: [...T], options: SchemaOptions = {}) {
  return Type.Unsafe<T[number]>({ ...options, [Kind]: 'StringEnum', enum: values })
}

// use as custom type
const T = StringEnum(['A', 'B', 'C'])   // TUnsafe<'A' | 'B' | 'C'>
type T = Static<typeof T>               // 'A' | 'B' | 'C'

Value.Check(T, 'A') // true
Value.Check(T, 'B') // true
Value.Check(T, 'C') // true
Value.Check(T, 'D') // false

By setting a custom validator on the TypeRegistry, this will allow you use Value.Check and TypeCompiler to check enum values. Hope this helps S

dswbx commented 1 month ago

@sinclairzx81 thank you very much, this works nicely! Any chance to modify the error message (so that I can specify that the kind is matched but the value is wrong)? (EDIT: found it and updated my example. Works great!)

I understand that you might have a different focus, since there are many validators around. But would you accept a pull request to add the enum-check on string types? As far as I can say that's a very common use-case, e.g. and the pattern is supported. I could probably add this inside this function (along with a unit test of course).

dearlordylord commented 1 month ago

as a way to overcome it you can do Value.Decode catching the error and narrowing the error type with Value.Errors

try {
    const r = Value.Decode(User, u);
    return { _tag: 'right', value: r };
  } catch (e) {
    // type of their errors is unknown, you should additionally call error list methods
    return { _tag: 'left', error: [...Value.Errors(User, u)] };
  }
sinclairzx81 commented 1 month ago

@dswbx Hi, Apologies for the delay in reply.

I understand that you might have a different focus, since there are many validators around. But would you accept a pull request to add the enum-check on string types? As far as I can say that's a very common use-case, e.g. and the pattern is supported. I could probably add this inside this function (along with a unit test of course).

Unfortunately, I can't accept any updates to the current Enum schematics at this time (mostly to lock down this aspect of the library so I've a basis for updating it). The eventual goal is to reshape Type.Enum into a { enum: [...] }, which requires fairly heavy refactoring of how TB currently handles Union types. The current setup is fairly consistent around anyOf (schematics, inference and validation) so just want to keep things as they are so I can investigate reshaping things from a consistent state (there is a lot of complexity down there)

Thanks for the offer tho, but can only really advise the using the approach shown above for now. Will close off this issue for now. All the best! S