-
-
Notifications
You must be signed in to change notification settings - Fork 292
Description
Hello,
I've been experimenting with Valibot, and I'm having a hard time trying to predict the behavior of schemas like optional(), nullish(), nonNullable(), etc...
I've been working on my custom parser that works with a stream of value fragments, instead of a complete one, to achieve basic progressive validation that is based on shape and lengths. The goal is to abort the stream early if any abnormalities were found.
So, in order to do that, I set up the following flags: nullAllowed, undefinedAllowed, isOptional, hasFallback, which would be used to simplify validating types and allowing null/undefined/missing values based on the behavior of the "wrapping" schemas. This is the best I could do for now and it's still on-going:
type SchemaType = v.GenericSchema & Partial<{
/** object */
entries: Record<string, SchemaType>
/** optional, nullish, non_nullable... */
wrapped: SchemaType
/** pipe (validation) */
pipe: (SchemaType & { requirement?: number })[]
/** union, variant, intersect */
options: SchemaType[]
/** array */
item: SchemaType
/** tuple */
items: SchemaType[]
/** tuple_with_rest, object_with_rest */
rest: SchemaType
/** record, map, variant */
key: SchemaType|string
/** record, map */
value: SchemaType
/** fallback */
fallback: unknown
/** metadata */
metadata: Record<string, unknown>
// Custom
nullAllowed: boolean|undefined
undefinedAllowed: boolean|undefined
hasFallback: boolean|undefined
isOptional: boolean|undefined
deepest: SchemaType
}>
const prepareSchema = (schema_: v.GenericSchema) => {
let schema = schema_ as SchemaType
let nullAllowed: boolean|undefined, undefinedAllowed: boolean|undefined,
hasFallback: boolean|undefined, isOptional: boolean|undefined
if (!('fallback' in schema)) isOptional = schema.type === 'exact_optional'
|| schema.type === 'optional'
|| schema.type === 'nullish'
while (schema.wrapped) {
if (hasFallback || 'fallback' in schema) {
hasFallback = true
schema = schema.wrapped
continue
}
switch (schema.type) { // TODO: match async variants
case 'undefinedable': undefinedAllowed ??= true; break;
case 'optional': undefinedAllowed ??= true;
case 'exact_optional': break;
case 'nullish': undefinedAllowed ??= true;
case 'nullable': nullAllowed ??= true; break;
case 'non_optional': undefinedAllowed ??= false; isOptional ??= false; break;
case 'non_nullish': undefinedAllowed ??= false; isOptional ??= false;
case 'non_nullable': nullAllowed ??= false; break;
}
schema = schema.wrapped
}
if (hasFallback || 'fallback' in schema) {
hasFallback = true
nullAllowed ??= true
undefinedAllowed ??= true
isOptional ??= true
}
console.log({ nullAllowed, undefinedAllowed, isOptional, hasFallback })
schema.hasFallback = hasFallback
schema.nullAllowed = nullAllowed
schema.undefinedAllowed = undefinedAllowed
;(schema_ as SchemaType).isOptional = isOptional
;(schema_ as SchemaType).deepest = schema
// ...
}
const schema = v.object({
test: v.exactOptional(v.nonOptional(
v.fallback(v.undefinedable(v.nonNullable(v.string())), 'abc')
))
})
prepareSchema(schema.entries.test)
const val = {
test: undefined
}
v.parse(schema, val)
console.log('pass')The problem with wrapped schemas is that they can be arbitrarily chained together, and some of the produced combinations are realistically not so useful but only add unnecessary complexity for a matter that can be solved by simply using flags. Not mentioning the fact that multiple nested objects aren't really memory efficient.
Taking fallback() as an example, it only modifies the "type" schema and adds a single .fallback property with the set value. I think the same can be done with undefined/null/optional.
I guess, there can be just one "modifier" schema that works like this:
let schema = v.object({
test: v.modifier(v.string(), { undefined: true, optional: true })
})And perhaps, other wrapping schemas can be either turned into aliases to v.modifier() with pre-set flags or scraped altogether. But that would break backwards compatibility.
Thoughts?