commons_validator 0.1.0
commons_validator: ^0.1.0 copied to clipboard
A Dart port of the Apache Commons Validator routines - email, URL, domain, IP, IBAN, ISBN, ISIN, ISSN, credit card, check digit, number and date validators.
commons_validator #
A Dart port of the validation routines from Apache Commons Validator, covering email, URL, domain, IP, IBAN, ISBN, ISIN, ISSN, credit card, check digit, number and date validation.
Pure Dart, so it works in Flutter, Dart CLI and server code alike. It pulls in no
dart:io and no dart:mirrors.
Ported from Apache Commons Validator 1.11.1-SNAPSHOT (git 770f7846).
Why this exists #
Nothing on pub.dev covers this ground. The closest packages are thin regular expression wrappers with none of the check-digit arithmetic, IBAN country registry, TLD-aware domain validation, or locale-aware number and date parsing that Commons Validator has accumulated over twenty years.
Install #
dependencies:
commons_validator: ^0.1.0
The intl constraint is deliberately loose (>=0.20.0 <0.21.0) because
flutter_localizations pins intl to an exact version; a tighter constraint
would make this package unusable in any app that uses Flutter's own
localizations.
Use #
import 'package:commons_validator/commons_validator.dart';
EmailValidator.getInstance().isValid('someone@example.com'); // true
UrlValidator.getInstance().isValid('https://example.com/a?b=c'); // true
DomainValidator.getInstance().isValid('exämple.com'); // true
IBANValidator.getInstance().isValid('GB29NWBK60161331926819'); // true
ISBNValidator.getInstance().validate('1-930110-99-5'); // 9781930110991
// Numbers and dates take an explicit locale or pattern - see "Locale handling".
BigDecimalValidator.getInstance().parse('1.234,56', locale: 'de_DE'); // 1234.56
DateValidator.getInstance().parse('2026-06-15', pattern: 'yyyy-MM-dd');
See example/commons_validator_example.dart
for a fuller tour.
What is ported #
The whole of org.apache.commons.validator.routines, plus the two helpers it
depends on.
| Java | Dart |
|---|---|
RegexValidator, CodeValidator |
RegexValidator, CodeValidator |
DomainValidator |
DomainValidator |
EmailValidator |
EmailValidator |
UrlValidator |
UrlValidator |
InetAddressValidator |
InetAddressValidator |
CreditCardValidator |
CreditCardValidator, CreditCardRange |
IBANValidator, IBANValidatorStatus |
IBANValidator, IBANCountryValidator, IBANValidatorStatus |
ISBNValidator, ISSNValidator, ISINValidator |
same names |
all 16 routines.checkdigit classes |
same names |
Byte/Short/Integer/Long/Float/Double/BigDecimal/BigInteger/Currency/Percent validators |
same names |
DateValidator, CalendarValidator, TimeValidator |
same names |
AbstractFormatValidator + AbstractNumberValidator |
AbstractNumberValidator<T> (merged, generic) |
AbstractCalendarValidator |
AbstractCalendarValidator<T> |
GenericValidator.isBlankOrNull, matchRegexp |
GenericValidator |
util.Flags |
Flags |
What is not ported #
The legacy org.apache.commons.validator root package — Validator,
ValidatorResources, ValidatorAction, Form, FormSet, Field, Arg,
Msg, Var — is not included and cannot be. It is an XML-configured engine
whose entire premise is resolving a class name, a method name and parameter type
names from strings at runtime and invoking them by reflection, driven by
commons-digester. Flutter has no runtime reflection, so there is no faithful
translation, only a redesign around code generation or a registry of closures.
For declarative form validation in Dart, reach for a combinator API or
build_runner codegen instead.
How closely does it match Java? #
Where Dart can match the JVM exactly, it does, and that is measured rather
than asserted. tool/jvm_diff/ compiles the real Java classes and runs the same
generated inputs through both implementations:
| Layer | Inputs | Disagreements |
|---|---|---|
| Check digits (all 16 routines) | 12,249 | 0 |
| Regex, code and financial validators | 30,664 | 0 |
| Domain, email, URL, IP | 46,892 | 0 |
java.net.IDN.toASCII (every BMP code point) |
130,240 | 0 |
| RFC 2396 URI splitting | 568 | 0 |
Number validators, en_US (incl. scientific patterns) |
1,547 | 0 |
format(), seven locales |
1,372 | 0 |
| Number validators, seven locales | 840 | 1 (CLDR data, below) |
| Date and time comparisons | 3,024 | 0 |
| Date and time parsing | 21,438 | 1 case (below) |
Calendar week numbering |
11,692 | 0 |
260,526 inputs, 7 disagreements in total, both documented below.
The corpora include every single-character substitution at every position, transpositions, truncations, control characters, NBSP, Arabic-Indic and fullwidth digits, scientific number patterns, two-digit years in every field position, and — for dates — every month/day combination across several years under four different week-rule settings.
Documented divergences #
These are the places the port deliberately or unavoidably differs. Nothing else is known to differ.
Locale handling is explicit, not ambient #
This is the largest deliberate change. In Java, passing null for a
Locale or TimeZone means "read the JVM global default", and Commons
Validator's own tests mutate that global to control the outcome. Dart has no
mutable default time zone and Intl.defaultLocale is a much weaker analogue, so:
- the number validators take a
locale:argument, falling back toIntl.defaultLocaleand then the system locale; - the date validators take a
pattern:and an optionalzoneOffset:, and read values as UTC when no offset is given.
Pass them explicitly and results are deterministic. This also means the ported tests assert against a stated locale instead of an ambient one.
Time zones are fixed offsets #
java.util.TimeZone becomes a Duration offset. Dart's DateTime supports only
local time and UTC, so full IANA zone support would mean depending on
package:timezone, and TimeZone.hasSameRules — rule equivalence rather than
identity — has no equivalent at all.
Decimal has no scale #
java.math.BigDecimal carries a scale, so Java prints 1234.00 where
package:decimal prints 1234. The values are equal and compare equal; only
the rendering differs. Truncation still happens where Java truncates: a strict
validator applies the pattern's scale with round-toward-zero, so 1234.567
under a two-decimal format is 1234.56, not 1234.57.
An extreme exponent is bounded, not materialised #
java.math.BigDecimal keeps an int scale beside its unscaled value, so Java
parses 1E20000000 in microseconds and reports it as 1E+20000000. Dart's
Decimal is rational-backed, so representing that means building a
twenty-million-digit integer — minutes of CPU and hundreds of megabytes, which
is a denial of service on untrusted input.
So a scale beyond ~1000 is not materialised:
DoubleValidatorandFloatValidatorstill answer infinity or zero, exactly as Java does, because the double is read straight from the digit string;BigDecimalValidator,BigIntegerValidatorand the integer validators return null instead of a value.
That last case is the divergence. It only affects exponents far past anything
real — a double tops out near 1e±324. Note upstream is no better placed here:
Java's BigIntegerValidator expands 1E1000000 into a 1,000,001-digit
BigInteger rather than declining.
ja_JP currency symbol #
Java's DecimalFormatSymbols gives U+FFE5 (fullwidth ¥); intl gives U+00A5
(¥). A Japanese currency amount written with the halfwidth sign validates here
and not on the JVM. This is the only locale-data difference found across
en_US, de_DE, fr_FR, ru_RU, sv_SE, it_IT and ja_JP — separators,
minus signs and affix positions otherwise agree exactly, including fr's U+202F
grouping separator, ru/sv's U+00A0, and sv's U+2212 minus.
An exponent in a date field #
Java parses date number fields with DecimalFormat, which accepts exponents, so
SimpleDateFormat("yyyy-MM-dd") reads 1E3-06-15 as the year 1000. This port
rejects it. The divergence makes the port stricter, and reproducing the quirk
would mean accepting nonsense as a date.
DateStyle is approximate #
Java's DateFormat.SHORT/MEDIUM/LONG/FULL map onto the nearest intl
skeletons. They are not identical — Java's SHORT for en_US uses a two-digit
year where intl's equivalent uses four. Pass an explicit pattern: when
exact agreement with the JVM matters; pattern-driven parsing is exact.
IDNA is 2003, not UTS-46 #
DomainValidator reproduces java.net.IDN.toASCII, which implements IDNA-2003
with RFC 3491 nameprep. IDNA-2008 and UTS-46 disagree with it on ß, final
sigma and the zero-width joiners. Every table in lib/src/internal/idna_data.dart is derived by measuring OpenJDK
17.0.18 itself rather than transcribed from the RFCs — guessing at the tables from
the specification produced 319 disagreements before that approach replaced it.
Regenerating against a different JDK would legitimately shift the
unassigned-code-point boundary, since a newer Unicode version assigns more code
points.
ISINValidator country codes are pinned #
Upstream reads its country list from Locale.getISOCountries(), live JDK data
Dart cannot reach. The list in lib/src/routines/isin_data.dart is a snapshot of
OpenJDK 17.0.18's 249 codes. A newer JDK may know more.
Web int is a JavaScript double #
The package does compile and run on Flutter web — that is verified, not
assumed. But int above 2^53 is inexact there, so:
LongValidatorcannot distinguish values near the 64-bit boundary as precisely as the JVM does. Its bounds are held asBigIntand built withBigInt.parse, because a literal9223372036854775807is a compile error for the web, not merely an imprecise value — one anywhere in the package would break every Flutter web build. Keep that in mind if you extend it.Flags.turnOnAllandFlags.toStringare not accurate above bit 52.UrlValidator's own option mask uses only bits 0–3, so it is unaffected.
Tests that depend on exact 64-bit behaviour are marked testOn: 'vm'.
Smaller, deliberate choices #
- No
Serializable, noclone().Flagsofferscopy()instead. AbstractNumberValidator<T>is generic, soIntegerValidator.parsereturnsint?andBigDecimalValidator.parsereturnsDecimal?. Java works inObjectand casts in every leaf.CalendarFieldis an enum, so theIllegalArgumentExceptionJava throws for an unsupported comparison field becomes a compile-time guarantee.CalendarFieldsis immutable. Java returns theDateFormat's own mutableCalendar; this returns a value type, with week rules configurable becausecompareWeeksdepends on them.DomainValidator.resetOverridesForTestingis not in the Java API. Upstream isolates its TLD-override tests with a fresh classloader per test, which Dart has no equivalent of.DomainValidator.isValidDomainSyntaxis public; it is package-private in Java.- Exception messages are close to but not byte-identical with Java's, since
Dart has no
String.format. No test asserts on message text except where upstream's own tests do.
Behaviours worth knowing about #
These are upstream's, not the port's, and they surprise people:
strictcontrols trailing input, not validity. A lenient number or date validator accepts1234abcas 1234 — a parse error always fails, but leftover input only fails when strict.CodeValidator.isValidisvalidate(x) != null, andvalidatereturns the reformatted code with separators stripped. So a code can be valid while its raw text would fail the check digit.- A zero weighted sum is invalid.
ModulusCheckDigitrejects all-zero codes. - Grouping placement is not checked.
1,2,3,4parses as 1234, as it does in Java. - Non-ASCII digits split two ways. The number validators accept Arabic-Indic
and fullwidth digits, because
DecimalFormatfalls back toCharacter.digit; the check-digit routines reject them, because their Java source checks explicitly. Both behaviours are reproduced. DomainValidator's TLD overrides are process-global and freeze on the firstgetInstance()call. PrefergetInstanceWithOverrides, which scopes them to one instance.
Regenerating the data tables #
Four files are generated and should not be hand-edited:
| File | Source |
|---|---|
lib/src/routines/domain_tlds.dart |
the String[] tables in DomainValidator.java |
lib/src/routines/iban_validators.dart |
IBANValidator.DEFAULT_VALIDATORS |
lib/src/routines/isin_data.dart |
Locale.getISOCountries() + ISINValidator.SPECIALS |
lib/src/internal/idna_data.dart |
the reference JDK: IDN.toASCII/toUnicode round trips, its rejections, and Character.getDirectionality |
lib/src/internal/unicode_digits.dart |
the reference JDK's Character.digit |
Each has a script, and each is reproducible: running it regenerates the committed data byte-for-byte.
dart run tool/generate_tlds.dart ../commons-validator
dart run tool/generate_iban.dart ../commons-validator
dart run tool/generate_isin_data.dart ../commons-validator
dart run tool/generate_idna_data.dart # needs a JDK on PATH
dart run tool/generate_unicode_digits.dart # needs a JDK on PATH
Note the two TLD tables come from different IANA snapshots upstream — generic TLDs from 2026-06-23, country codes from 2024-04-02 — so do not refresh one in isolation.
Licence #
Apache License 2.0, as a derivative work of Apache Commons Validator. See
LICENSE and NOTICE.