Validate columns against rules that name their definition: the email rule browsers enforce, a URL scheme allowlist, calendar-checked dates, and every failure explained
Initializing in your browser…
Classify every column against a decider rather than a regex: calendar-checked dates, RFC 4291 addresses, and leading zeros kept out of integer columns
Count what is missing in five categories that add up to the grid, with the tokens that mean missing your choice
Format and validate against RFC 4180 by section, with output verified by reading it back
Before an import you need every email well formed, every age at least 18, and every link a real web address.
Rules
email: Email · age: Min value 18 · site: URL
Report
6 errors in 3 of 4 rows row 3 email the domain has an empty label between two dots row 3 age 17 is below the minimum of 18 row 3 site the scheme is javascript:, and this rule allows http: and https: row 4 site no scheme; a bare host is not a URL -> try https://example.com row 5 age "12abc" is not a number, so it cannot be compared to a bound
Each failure names the rule and the reason. `bob@example..com` fails because the WHATWG production browsers enforce requires every domain label to start and end with an alphanumeric, which the usual email pattern does not check. `javascript:alert(1)` fails because a URL rule allows http and https by default, and that is what stops a script being stored in a link column. And `12abc` fails a numeric bound rather than passing it as 12, which is what parseFloat would have made it.
Check every cell in a column against a rule and get told exactly which values fail and why. The difference from most validators is what each rule actually enforces: the email rule is the one browsers enforce, the URL rule has a scheme allowlist so it refuses javascript:, the date rule goes through the calendar so it refuses 29 February 2025, and the phone rule counts digits rather than characters.
A validator that rejects something valid is annoying. A validator that ACCEPTS something invalid is the entire failure mode, because the file then passes and the bad row is discovered by whatever consumes it. Measured on 2026-09-01 against a 126-case corpus, with every corpus answer cross-checked against an independent decider first, the previous version of this tool was right on 94 cases and accepted 24 values that should have been rejected.
The email pattern was the usual one, which says nothing about the shape of a domain label. It accepted someone@example..com (an empty label between two dots), someone@.example.com, someone@example.com., someone@-example.com and someone@example-.com. It also rejected someone@localhost, which is a valid address: the WHATWG HTML production every HTML5 form enforces deliberately allows a domain with no dot. The rule here is that production, which gets all seven right because it requires every label to start and end with an alphanumeric.
The URL rule was `new URL()` alone, and the WHATWG URL parser is not a URL validator: it parses javascript:alert(1), data:text/html with markup in it, file:///etc/passwd and foo:bar quite happily. A column of web addresses that accepts a javascript: value is storing something that will later be rendered as a link. The rule here is the parser plus a scheme allowlist, http and https by default, which you can widen per rule; the failure names the scheme it saw and says why it matters, and a bare host like example.com is refused with the https:// form suggested.
The date rule was `new Date()`, which is not a date validator either. It accepted 2025-02-29 (2025 is not a leap year) and 2025-04-31 (April has 30 days), and, worse, the bare strings 2025, 5 and 0, because new Date("5") is 1 May 2001. The rule here goes through the calendar, and a bare number is refused with a message saying it is a number rather than a date.
The phone rule counted characters between 7 and 20 from a set that included dashes, spaces, plus signs and brackets, so seven dashes, seven brackets and seven plus signs were all valid telephone numbers, and so was 2025-03-04. The rule here counts DIGITS, requires between 7 and 15 of them because E.164 caps an international number at 15, refuses anything shaped like a date or a time, and allows at most one balanced bracketed group.
The numeric bounds used parseFloat, and parseFloat("12abc") is 12, so 12abc passed a "minimum 10" rule. A bound is only meaningful on something that is entirely a number, so it now refuses anything that is not and says which characters were the problem. And the regex rule was an unanchored test, which is a substring match: a pattern of five digits passed abc12345xyz. Patterns are now anchored to the whole value by default, with a switch for the substring behaviour, so whichever you get is visible rather than a surprise.
Every failure carries a reason naming the rule and, where there is one, a concrete next step: the nearest entry in an allowed list, the https:// form of a bare host, how many characters to remove. And because the results replace the rules panel, the results now carry a summary of what was checked, so the definition each rule enforces is still on screen at the moment an error makes you want it.
Find the rows a database or an API will reject before you send them, with the reason for each, rather than after a partial load.
Catch the malformed addresses a common email pattern lets through, such as a domain with an empty label or one starting with a dash.
Refuse javascript: and data: values in a URL column, which are exactly what a permissive check lets through and what later gets rendered as a link.
Use an in-list rule to require one of a fixed set of values, and get the nearest allowed entry suggested for each near miss.
Because it is, under the definition browsers enforce. The WHATWG HTML valid e-mail address production allows a domain with no dot so that intranet hosts work, and a real <input type="email"> accepts it. The same definition is what refuses someone@example..com and someone@-example.com, which the usual pattern accepts.
The URL rule allows http and https by default, because that is what a column of web addresses normally holds, and because the same allowlist is what refuses javascript: and data:. Each URL rule has its own list of allowed schemes, so add ftp to it and the value passes.
By default, yes, and the failure message says so. An unanchored test is a substring match, which means a pattern of five digits would pass abc12345xyz, and almost nobody means that. Turn off "Match the whole value" on the rule if you do.
By default an empty cell fails whatever rule is on the column, which is what you want when the column is mandatory. Turn on "Skip empty cells" for an optional column, and add a separate Required rule where presence itself is the thing you are checking.
Because it is not a number. parseFloat reads it as 12, which is how it used to pass, but a numeric bound only means something on a value that is entirely a number. The error says which characters made it fail.
Required accepts any cell that is not empty, including one holding only a space. Not blank requires at least one character that is not whitespace.
Everything runs in your browser and the uniqueness check indexes each column once rather than rescanning it per cell, so 10,000 rows with a uniqueness rule complete in about 25 milliseconds.
Rows and columns are parsed and transformed in memory in your browser. No record ever reaches a server.