zod
zod copied to clipboard
Transform strings to normalize URLs
Problem
Users typically do not include the protocol when hand-typing links. As such, applying z.string().url() leads to unintuitive use experience when validating user input. Further a custom transformation (ex: z.string().transform(normalizeUrlLikeString).url()) is not supported by Zod.
Proposed solution
A .toUrl() transformation function that will--
- Add
https://if the string does not include a protocol - Validate the string as a URL
- Return the new/valid string.
Is this what you are looking for?
const schema = z.union( [
z.string().url(),
z.string().transform( x => x.startsWith( 'https://' ) ? x : `https://${ x }` )
] )
console.log( schema.parse( 'foo.com/bar/baz' ) ) // https://foo.com/bar/baz
If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏 https://github.com/sponsors/JacobWeisenburger
I understand the desire for something like this and your use case makes sense. For now I recommend using pipe.
z.string().transform(normalizeUrlLikeString).pipe(z.string().url())
Hopefully it's clear why you can't call .url() after .transform(), since your transform could return a value of any data type, not just strings.
Zod could to support some way to mutate input in a non-type-transforming way. This would differ from transform, which lets you transform the input at will and return a value of any type. The .mutate method would return an instance of the same class, instead of a ZodTransform.
z.string().mutate(val => {
return val.startsWith("http") ? val : `http://${val}`
})
// => returns a ZodString instance
Mutations would differ from transforms in that they can't modify the inferred type. Zod would enforce this statically. The only argument against is that I think this distinction would be lost on most people and there's potential for confusion.
It would perhaps be less confusing to somehow support this inside superRefine. You could return the new value from .superRefine, for instance. Or .superRefine could provide a method like resolve on the ctx object (similar to the notion of Promise's resolve.)
z.string().superRefine((val, ctx) => {
ctx.val; // access input data
ctx.addIssue(..); // add issues (similar to super refine)
ctx.resolve(val.startsWith("http") ? val : `http://${val}`);
});
Still weighing these options. Leaving open for discussion.
Maybe this could just be solved with an option passed to the .url()? Like z.string().url({ protocolOption: true })? Best is probably just to handle it via pipe though, as I think the way this is handled is very custom depending on the case.