validart 1.3.0
validart: ^1.3.0 copied to clipboard
A type-safe validation library for Dart, inspired by Zod. Supports parse/safeParse, transforms, coercion, schema composition, and structured errors.
Changelog #
1.3.0 - 2026-04-23 #
Added #
V.coerce.date()now accepts the same default formats asV.string().date()— ISO 8601 (YYYY-MM-DDand variants with time), BR (DD/MM/YYYY), US (MM/DD/YYYY), EU (DD.MM.YYYY), dashed (DD-MM-YYYY,MM-DD-YYYY), compact (YYYYMMDD), slashed (YYYY/MM/DD). Calendar-invalid dates (like30/02/2024) continue to throw. Parsing logic moved to a shared helper (lib/src/utils/date_parser.dart) used by bothDateStringValidatorandVCoerce.date(). For strict format validation, chainV.string().date(format: '...')before coercion.
Changed #
- Breaking —
V.string().phone()/.postalCode()/.taxId()/.licensePlate()now accept a list of patterns. The parameter was renamed frompattern:(singlePattern) topatterns:(non-emptyList<Pattern>), and validation succeeds when any pattern in the list matches — enabling multi-country systems to declare every accepted format in a single schema instead of wrapping multiple schemas inV.union([...]).V.string().phone()with no argument continues to default to a singleE164PhonePattern(behavior unchanged). Migration: wrap any existing single pattern in a list —pattern: const UsZipPattern()→patterns: [const UsZipPattern()]. For phone, the emitted error code keeps each pattern's customcodewhen exactly one pattern is configured; with two or more, the genericVStringCode.phoneis emitted. For postal code / tax ID / license plate, the{name}interpolation param now joins each pattern'snamewith/when multiple are configured — a single template like'Invalid {name}'renders correctly in both single- and multi-pattern mode. - Breaking — all error codes are now domain-prefixed. Every string emitted by
error.codefollows<type>.<action>(e.g.string.email,number.positive,int.even,bool.is_true,date.weekday,array.unique,enum.invalid). The three generic fallbacks (required,invalid_type,custom) stay flat inVCode—VLocaleuses them as the backstop when a prefixed key has no match. Constants in the sealed classes keep the same identifiers (VStringCode.email,VIntCode.even,VDoubleCode.integer, etc.); only the string value each one maps to changed. Migration: find-and-replace the old flat codes inVLocaleconfigurations and in anyerror.code == '...'comparisons. Highlights:'invalid_email'→'string.email','positive'→'number.positive','even'→'int.even','decimal'→'double.decimal','is_true'→'bool.is_true','weekday'→'date.weekday','unique'→'array.unique','invalid_enum'→'enum.invalid'. Complete mapping table in README. VLocale._defaultsis now structured as a nested map grouping each type's translations ('string': {'email': '...', 'too_small': '...'}). Default behavior unchanged — the_resolvefallback chain already works with flat or nested lookups. This is purely a readability improvement for maintainers; custom translations can still use either form.
Fixed #
- Missing default locale templates for 14 emitted codes. The codes
string.base64,string.cvv,string.hex_color,string.iban,string.json,string.mac,string.mongo_id,string.nano_id,string.semver,string.ulid,date.age,string.postal_code,string.tax_idandstring.license_platewere emitted by their validators but had no matching entry inVLocale._defaults, soerror.messagefell through to the raw code string (e.g."string.base64"instead of"Invalid Base64"). The default templates already documented in the README are now actually registered inVLocale. A regression test intest/src/messages/messages_test.dart('Every emitted VCode has a default translation') iterates everyVXxxCodeconstant and asserts a non-code default exists, so any new code added without a matching locale entry fails the suite immediately. - Invalid SIN example in README and
example/example.dart. The sampleV.string().taxId(patterns: [const CaSinPattern()]).validate('046-454-286')was commented// true, but SINs starting with0are forbidden by CRA specification — the actual output isfalse. Replaced with130-692-544(a Luhn-valid SIN starting with a legal leading digit). All README examples with// resultcomments were re-verified against real output after the change.
1.2.0 - 2026-04-23 #
Added #
ValidationModeenum (any/formatted/unformatted) — controls whether separator-based formatting is required, forbidden, or optional on validators that accept multiple input shapes. Exported frompackage:validart/validart.dart.modefield onUsSsnPattern,UkNiNumberPattern,CaSinPattern— SSN, NINO and SIN patterns now accept aValidationMode. Defaultanykeeps the current behavior (modulo the change noted below for SSN).modefield onCaPostalCodePattern,UkPostcodePattern— Canadian and UK postal codes can now require or forbid the separating space.modefield onUkPlatePattern— UK plates can now require or forbid the space between the two groups.modeparameter onVString.card()—V.string().card(mode: ValidationMode.formatted)requires groups of four separated by spaces/dashes;ValidationMode.unformattedrejects any non-digit;ValidationMode.any(default) keeps the current behavior.CountryCodeFormatenum (required/optional/none) andcountryCodefield onE164PhonePattern— pin whether the leading+must be present, optional (default), or forbidden. Exported frompackage:validart/validart.dart.VString.integer()/VString.numeric()— validate that a string is parseable as a Dartint(decimal base) or a finitedouble(accepts decimal and scientific notation, rejectsNaN/Infinity). Both keep the output type asString; useV.coerce.int()/V.coerce.double()when you want conversion instead. New error codesVCode.stringInteger(string.integer) andVCode.stringNumeric(string.numeric) — domain-prefixed so they can be translated independently ofVCode.integer(used byVDouble.integer()).- README i18n reference — new "Complete translation template" section in the i18n docs listing every translatable key with its default English message (including the new type-prefixed
required/invalid_typeentries and all interpolation tokens), ready to copy into aVLocaleand translate.
Changed #
- Breaking — Type-prefixed error codes for
requiredandinvalid_type. Every schema now emits its own prefixed code:VString→string.required/string.invalid_type,VInt→int.required/int.invalid_type, and so on forVDouble,VBool,VDate,VArray,VMap,VObject,VEnum,VLiteral,VUnion. The generic codes ('required','invalid_type') are no longer emitted at runtime but remain defined inVCodeas fallback keys for translation. Code that compareserror.code == 'required'must be updated to check the prefixed form or use.endsWith('.required'). - Breaking —
VCodereorganized into sealed classes per type. The 60+ flat constants (VCode.stringRequired,VCode.invalidEmail,VCode.positive,VCode.even, ...) were replaced by 12 companionsealed classes —VStringCode,VNumberCode,VIntCode,VDoubleCode,VBoolCode,VDateCode,VArrayCode,VMapCode,VObjectCode,VEnumCode,VLiteralCode,VUnionCode.VCodeitself now holds only the generic fallbacks (required,invalidType,custom). Migration is a mechanical rename — the strings emitted inerror.codeand the keys used byVLocalestay identical. Examples:VCode.stringRequired→VStringCode.required,VCode.invalidEmail→VStringCode.email,VCode.positive→VNumberCode.positive,VCode.even→VIntCode.even,VCode.age→VDateCode.age,VCode.invalidEnum→VEnumCode.invalid. VLocalenow accepts nested overrides and falls back to the generic code. Entries can be flat ('string.required': '...') or nested ('string': {'required': '...'}), mixed in the same map. When a prefixed code has no match, lookup drops the prefix and tries the generic key — so{'required': 'Obrigatório'}still covers every schema, while{'string': {'required': '...'}}or{'string.required': '...'}narrows the override to strings only. ExistingVLocale({'required': 'x'})calls keep working unchanged.VTypeexposestypeNamegetter — abstract in the base class, overridden by each concrete schema (e.g.'string','int'). Third-party schemas extendingVTypemust implement it.- Breaking (subtle) —
UsSsnPatterninValidationMode.anyno longer accepts mixed shapes like123-456789or12345-6789. Only fully-formatted (123-45-6789) or fully-unformatted (123456789) inputs are accepted. Users that relied on the older permissive behavior can write their own regex via a customTaxIdPattern.
1.1.0 - 2026-04-21 #
Added #
Async validation
refineAsync— async custom check on any schema. Companion consumersvalidateAsync,parseAsync,safeParseAsync,errorsAsyncrun the full pipeline asynchronously. Sync consumers (validate,parse,safeParse,errors) throw the newVAsyncRequiredExceptionwhen called on a schema that contains async steps — pointing callers to the async variant. Propagates recursively throughVMap,VArray,VObject,VUnion, andVTransformed. Schemas without async stay 100% synchronous with no overhead.AsyncValidator<T>— async sibling ofValidator<T>(returnsFuture<Map<String, dynamic>?>) with newaddAsyncmethod on every schema for plugging custom async validators. Reusable across schemas.preprocessAsync— async input transformation before type check.transformAsync<O>— async output conversion after validation (createsVTransformedAsync<I, O>).timeoutparameter onrefineAsync— exceeding it counts as a failure with the same code/message.VAsyncRequiredException— exception thrown by sync consumers when the schema contains async steps; includesmethodNameandsuggestionfields.
Generic string validators
base64— RFC 4648 (length multiple of 4, padding with=).hexColor—#FFFor#FFFFFF.mac— MAC address with colon or dash separators (consistent within the value).semver— Semantic Version 2.0 including pre-release / build metadata.mongoId— 24-hex MongoDB ObjectId.iban— ISO 13616 (accepts spaces) + mod-97 check digit.json— string that parses viadart:convert.cvv— 3 or 4 digits.ulid— 26 chars Crockford Base32, timestamp-cap first char (0–7).nanoId— URL-safe[A-Za-z0-9_-], default length 21 (configurable vialength).
Pluggable patterns
PhonePattern— abstract phone-validation strategy. Built-in:E164PhonePattern(default).V.string().phone({PhonePattern? pattern, String? message})— optionalpattern; without it, behaves exactly as before.CardBrandPattern— abstract card-brand matcher. Built-ins:VisaBrand,MastercardBrand,AmexBrand,DinersBrand,DiscoverBrand,JcbBrand.V.string().card({List<CardBrandPattern>? brands, String? message})— whenbrandsis omitted/empty, any Luhn-valid number is accepted; when provided, must match at least one.PostalCodePattern— abstract postal-code strategy. Built-ins:UsZipPattern(12345/12345-6789),CaPostalCodePattern(A1A 1A1),UkPostcodePattern.TaxIdPattern— abstract tax-ID strategy. Built-ins:UsSsnPattern(123-45-6789),UkNiNumberPattern(HMRC prefix rules +A–Dsuffix),CaSinPattern(9 digits + Luhn + CRA first-digit rule).LicensePlatePattern— abstract license-plate strategy. Built-in:UkPlatePattern(AB12 CDEpost-2001 format). Plates with heavy regional variation (US per state, CA per province) are intentionally not built in.
External packages (e.g. validart_br with CPF, CNPJ, CEP, Mercosul) can extend each abstract pattern without forking the core.
New methods / API
VString.date({String? format, String? message})— new optionalformat. When passed, the string must match that exact format (tokens:YYYY,MM,DD; any other character is a literal separator). When omitted, accepts ISO extended/basic, BR, US and EU layouts.VString.toPascalCase,toCamelCase,toSnakeCase,toScreamingSnakeCase,toSlug— pre-processing case transforms. Word boundaries detected across separators (,_,-), case transitions, and digits; non-alphanumeric characters are dropped.keepAccentsparameter on all case transformers. Defaultfalse(accents transliterated:São João→sao-joao,ç→c,ñ→n,ß→ss). Passtrueto preserve.VDate.age({int? min, int? max, String? message})— validates age derived from a birthdate (computed againstDateTime.now()at validation time). At least one ofmin/maxis required.UuidVersionenum —UuidVersion.v1throughUuidVersion.v8.V.string().uuid({UuidVersion? version, String? message})accepts an optional version filter — e.g.V.string().uuid(version: UuidVersion.v7)for timestamp-ordered only.VString.url({Set<String>? schemes})— optionalschemesset. Default{http, https}(backwards compatible); pass{http, https, ftp, ws}etc. to accept other protocols.VString.password({String? specialChars})— optionalspecialCharsstring. Default!@#$%^&*(),.?":{}|<>(backwards compatible); pass a custom string to expand (e.g.r'!@#$%^&*()-_+=<>?'to accept-and_).- Factory-level
messageon every factory —V.string(),V.int(),V.double(),V.bool(),V.date(),V.map(),V.array(),V.object(),V.enm(),V.literal(),V.union()all accept an optionalmessageto customize therequirederror per schema without changing the global locale. Example:V.bool(message: 'You must accept the terms').isTrue(). Fallback: custom → locale → default English. Same parameter name as the validator-levelmessage(.email(message: ...),.min(n, message: ...),.refine(fn, message: ...), ...) but different scope: the factory-level one fires only on null input (pre-validationrequirederror), the validator-level one fires only when that validator rejects a non-null value. Both can coexist on the same schema. hasDefault/defaultValueOrNullon every schema — read-only introspection getters onVType<T>.hasDefaultreturns whether a default was configured viadefaultValue(x);defaultValueOrNullreturns the configured value (ornullwhen none was set). Useful for tooling and conditional logic that needs to inspect schema state without altering it.
Changed #
UuidValidatornow accepts versions 1–8 (was 1–5). v6, v7 and v8 from RFC 9562 draft (including the timestamp-ordered v7 that is replacing v4 in many modern APIs) are considered valid.PhoneValidatordelegates to aPhonePatterninstead of hardcoding the E.164 regex. Backwards compatible: the default pattern preserves the previous behavior.CardValidatorgained an optionalbrandsconstructor parameter. Backwards compatible: withoutbrandsit behaves exactly as before (Luhn + length). Cards with masks (spaces or dashes) are still accepted.DateStringValidatorgained an optionalformatconstructor parameter and, without it, now accepts multiple formats (ISO extended/basic, BRDD/MM/YYYY, USMM/DD/YYYY, EUDD.MM.YYYY, and dashed variants). Ambiguous strings like02/03/2020pass when at least one interpretation is calendar-valid.- Breaking —
defaultValueis now validated by the pipeline. When input isnulland a default is set, the default is substituted for the input and runs through preprocessors, validators and refines (sync or async). Previously it short-circuited withVSuccess(default)regardless of whether the default satisfied the schema. This prevents silent bugs likeV.string().defaultValue('').min(3).parse(null)returning''. Upgrade: ensure yourdefaultValue(x)satisfies the chained validators. - Breaking —
DateStringValidatormulti-format mode. Callers that relied on the previous ISO-only semantics need to either usedate(format: 'YYYY-MM-DD')explicitly or accept that BR/US/EU strings now pass.
Fixed #
DateStringValidatornow rejects calendar-invalid dates such as2024-02-30,2024-04-31, or2023-02-29that the previous regex-only implementation accepted. The validator reconstructs the date viaDateTimeand compares each component to guard against rollover (2024-13-01→2025-01-01).
1.0.0 - 2026-04-18 #
Complete Rewrite #
Core API:
Vstatic class — no instantiation needed (V.string(),V.int(),V.map(), etc.)parse(),safeParse(),validate(),errors()on all typesVResult<T>sealed class withVSuccess<T>andVFailure<T>(Dart 3 pattern matching)VErrorwithcode,message,path(nested paths like['address', 'zip']or[2, 'name']), and optionalcontext(List<List<VError>>?) populated byVUnionto expose each option's failure
Types:
VString— 22 validators +notEmpty()+ pre-processing transforms (trim,toLowerCase,toUpperCase) that always run before validationVInt,VDouble— numeric validators (min,max,even,odd,prime,finite, etc.)VBool—isTrue,isFalseVDate—after,before,between,weekday,weekendVArray<T>—min,max,unique,contains+ indexed error pathsVMap— schema composition (pick,omit,extend,merge,partial,strict,passthrough) +equalFields(),refineField(),when(),whenRules,array().extend/mergepreserve pipeline state from the base (validators,whenrules,strict/passthrough/nullableflags,defaultValue, preprocessors);mergeconcatenates rules/steps from both sides and OR-s flagsVObject<T>— validates class/entity instances with type-safe field extractionVEnum<T>,VLiteral<T>,VUnionVTransformed<I, O>— type-changing transforms
Pipeline:
- Three-phase execution: pre-processing → validation → post-processing
- Pre-processing transforms (
trim,toLowerCase,toUpperCase) always run before validators regardless of chain order
Features:
- Coercion:
V.coerce.int(),V.coerce.double(),V.coerce.string(),V.coerce.bool(),V.coerce.date() transform<O>()— change output type with chaining supportpreprocess()— transform input before type checking; multiple calls chain in declaration orderequalFields()— cross-field comparison (password confirmation)when()— conditional field validationrefineField()— targeted cross-field validation whose error attaches to the specified field pathVFailure.toMap()— convert errors toMap<String, String>for Flutter formsdefaultValue(T)— provide defaults when value is null; takes precedence overnullable()when both are set- Fluent chaining across inherited and concrete methods via covariant returns on every concrete type — e.g.
V.string().nullable().email(),V.int().positive().even()
i18n:
VLocale—Map<String, String>with{param}interpolation; asserts in debug mode when any{param}is left unreplacedV.setLocale()/V.t()— switch locale at runtimeVCode— static string constants for all error codes (extensible by external packages)- Fallback chain: custom translation → English default → code itself
- Per-validator message override bypasses locale
Architecture:
- Validators are classes extending
Validator<T>invalidators/{type}/— returnMap<String, dynamic>?(params, not messages) add()is public — external packages can extend types via Dart extensionspart ofpattern for type files sharing pipeline internals- Zero production dependencies
Removed (from v0.x) #
Validartclass (replaced byVstatic class)VMessages,VStringMessages, etc. (replaced byVLocale)getErrorMessage()method- Brazilian validators (CPF, CNPJ, CEP) — to be published as
validart_brpackage any()/every()combinators (replaced byVUnionandrefine())