z.string().url() allows invalid urls like 'http:/www.apple.com'
z.string().url() validator incorrectly allows urls with a single forward slash after the protocol, such as "http:/www.apple.com". This occurs because the validation relies on the native new URL() constructor, which is too lenient for the purpose of strict data validation. new URL() fixes the url, but z.string().url() should just fail in this case.
Hi @kirillku! I'm Dosu and I’m helping the zod team.
You're right—z.string().url() in Zod uses the native JavaScript URL constructor for validation, which is quite permissive and will accept URLs like "http:/www.apple.com" with only a single slash after the protocol. This is intentional and documented behavior; all strictness comes from the environment's URL implementation, not from Zod itself (docs, code).
If you need stricter validation, you can use protocol and hostname regex constraints, or the new httpUrl() helper in Zod 4, which restricts the protocol to http/https and validates the hostname against a domain regex (docs, PR):
const schema = z.url({
protocol: /^https?$/,
hostname: z.regexes.domain
});
For even stricter checks (like enforcing double slashes after the protocol), you can add a custom .refine() or regex:
const strictUrl = z.string().refine(url =>
/^https?:\/\/.+/.test(url) && (() => { try { new URL(url); return true; } catch { return false; } })(),
{ message: "Invalid URL format" }
);
Let me know if you need a more specific example or if this answers your question—if so, please close the issue! Tagging @colinhacks for visibility.
To reply, just mention @dosu.
How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
@kirillku, you're right. This is interesting:
z.url().parse("http:/www.apple"); // returns "http:/www.apple"
z.httpUrl().parse("http:/www.apple"); // returns "http:/www.apple"
Note that setting the normalize flag fixes this:
z.url({ normalize: true }).parse("http:/www.apple"); // returns "http://www.apple/"
z.httpUrl().parse("http:/www.apple"); // returns "http://www.apple/"
But we have nothing like what @kirillku is suggesting.
@colinhacks, consider how this compares with objects to see what's missing:
| Zod | Action | Zod | Action | Summary | ||
|---|---|---|---|---|---|---|
z.object |
Strips keys by default | z.url({ normalize: true }) |
Fixes invalid urls | Coerces to the true type | ||
z.looseObject |
Allows any keys | z.url() |
Leaves url unchanged | Returns input unmodified | ||
z.strictObject |
Throws on extra keys | ??? |
Requires a valid url | Requires correctly matching input |
Would you agree we need something like this?
I know my next thought might be a breaking change (feel free to shoot it down), but note that the pattern you've set for objects is intuitively safe. That is, z.object - the shortest and most common variant - ensures the output exactly matches the pattern. We have to write something longer (z.looseObject) to explicitly say we want to allow other unknown keys to pass through (which could become unsafe, like with "__proto__").
Right now, the default behavior for z.url is the not safe variant, e.g. z.looseObject. You don't actually always get a valid url back, as shown. My suggestion is simply to make normalize default to true instead of false.