Skip to content

Wrapped schemas are overkill? #1357

@iMrDJAi

Description

@iMrDJAi

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?

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions