fabian-hiller / valibot

The modular and type safe schema library for validating structural data 🤖
https://valibot.dev
MIT License
6.32k stars 204 forks source link

Handle empty strings as undefined #892

Closed stackoverfloweth closed 3 weeks ago

stackoverfloweth commented 1 month ago

when I have schemas with

v.object({
  foo: v.optional(v.string())
})

as the user backspaces to an empty input the control will usually emit a value of "".

I played around with using v.fallback to circumvent any validation errors but what I was hoping for is a rule that will actually translate an empty string to undefined.

JacKyDev commented 1 month ago

I'm not sure if I understand you correctly, but if you mean something like a checkbox that returns "true" or "" (aka false), then the solution would be something like this.

import * as v from 'valibot';

v.object({
  foo: v.optional(v.pipe(v.string(), v.transform((val) => {
    return val == "true";
  })))
})

The schema indicates that the field is generally optional, but if filled, it can ultimately represent a Boolean with the value true|false.

and if you want only booleans as result you can define the defaultValue of Optional as "false" like

import * as v from 'valibot';

v.object({
  foo: v.optional(v.pipe(v.string(), v.transform((val) => {
    return val == "true";
  })), "false")
})

And if you really want a undefined | string

v.object({
    foo: v.optional(v.pipe(v.string(), v.transform((val) => {
        return val.trim() === "" ? undefined : val;
    })))
})
stackoverfloweth commented 4 weeks ago

thanks for the input @JacKyDev. Let me provide a more nuanced example

const stringToNumberSchema = v.pipe(
  v.string(),
  v.decimal(),
  v.transform(Number),
);

const test = v.optional(
  v.pipe(stringToNumberSchema, v.minValue(0), v.maxValue(100)),
);

In this situation, when the value "" is sent to test I get the error

Invalid decimal: Received \"\",

In an attempt to apply your suggestion above I ended up with this

const stringToNumberSchema = v.pipe(
  v.string(),
  v.transform((value) => (value.trim() === '' ? undefined : value)),
  v.decimal(),
  v.transform(Number),
);

const test = v.optional(
  v.pipe(stringToNumberSchema, v.minValue(0), v.maxValue(100)),
);

however, this has a TS error on v.decimal()

Type 'undefined' is not assignable to type 'string'

and at runtime still produces an error

Invalid decimal: Received undefined

valibot playground

JacKyDev commented 4 weeks ago

I would rather have the question of what speaks against using 0 as the value when the input is an empty string?

The code would be as follows:

import * as v from 'valibot';

const stringToNumberSchema = v.pipe(
  v.string(),
  v.transform((value: string) => (value.trim() === '' ? 0 : Number(value))),
);

const test = v.optional(
  v.pipe(stringToNumberSchema, v.minValue(0), v.maxValue(100)),
);

const result = v.safeParse(test, '');

console.log(result);

The following aspects would result:

  1. If empty string, then Success => Output 0
  2. If 0 - 100, then Success => Output 0 - 100
  3. If < 0 || > 100, error because the value is invalid
  4. If undefined, successful with undefined.

If 0 as a value makes sense, you could also extend the optional with the following line:

...

const test = v.optional(
  v.pipe(stringToNumberSchema, v.minValue(0), v.maxValue(100)),
  "0"
);

...

This would result in an output of 0 even when the value is undefined, and the schema would always be of type number in a successful case.

Furthermore, I don't understand the check for decimal because the schema only requires that the value is a number. In that case the problem is that empty string is not a valid decimal string and failed.

I hope this helps; otherwise, feel free to provide a bit more context so I can better understand.

fabian-hiller commented 4 weeks ago

Here is another option. Try it out in our playground.

import * as v from 'valibot';

const Schema = v.optional(
  v.union([
    v.pipe(
      v.literal(''),
      v.transform(() => undefined),
    ),
    v.pipe(
      v.string(),
      v.decimal(),
      v.transform(Number),
      v.minValue(0),
      v.maxValue(100),
    ),
  ]),
);