validart 3.0.0
validart: ^3.0.0 copied to clipboard
Type-safe validation for Dart, inspired by Zod. Validate typed entities and raw JSON-like maps with the same schema — structured errors, async, i18n built-in.
Changelog #
3.0.0 - 2026-05-17 #
Added #
VObject<T>.safeParseRaw/parseRaw/validateRaw/errorsRaw(and async siblings). Validate aMap<String, dynamic>directly against an object schema without first constructingT. Each declared field is read by name (map[field.name]); per-field validators, transforms,whenrules,whenMatchesRawrules,refineFieldRawrules,strict, andpassthroughapply. Returns the validated map — caller constructsTafterwards with the guarantee that every required field is present. Motivation: whenT's constructor rejects null on a required field, building it for a partial payload throws before any validator can run;safeParseRawremoves the friction. Entity-level rules typed againstT(refine,refineField,equalFields) and entity-level transforms are silently skipped — pass the validated map throughT's constructor and revalidate withsafeParse(T)when those rules matter. Schemas declaringwhenMatches(entity-only) throwVExceptionat thesafeParseRawentry point — usewhenMatchesRawinstead.defaultValueis a no-op (typed asT?, cannot substitute intoMap);nullable()is honored. Pinned bytest/src/types/object_raw_test.dartand the newVObject.safeParseRaw contractgroup inpipeline_contract_test.dart.VObject<T>.strict()/.passthrough()— previously unavailable onVObject, now exposed with documented semantics. No-op in entity mode (declared fields are read through extractors, so unknown keys cannot arrive). Active in raw mode:strict()emitsobject.unrecognized_keyfor every key not declared on the schema;passthrough()copies non-declared keys through to the validated map. MirrorsVMap.strict()/passthrough(). State propagates throughpick/omit/merge/partial(via_copyObjectStateTo).VObjectCode.unrecognizedKey('object.unrecognized_key') — emitted bysafeParseRawon aVObjectmarked.strict()for each undeclared key. Default template:'Unrecognized key "{key}"'. Customizable globally viaVLocale({'object.unrecognized_key': '...'})or per-call. Pinned by the regression group intest/src/messages/messages_test.dart::VObjectCode.RefineStageenum (post/pre) — controls when arefineField/refineFieldRawcallback runs in the container pipeline.post(default) runs after per-field iteration and is gated bydependsOn;preruns before per-field iteration and sees the input as the user typed it (no preprocess / transforms applied). Exported frompackage:validart/validart.dart.V.date()calendar helpers —isToday(),sameDayAs(DateTime other),afterToday(),beforeToday(). Compare only y/m/d (hour/minute/second ignored), resolved againstDateTime.now()in local time at validation time.isToday/sameDayAsaccept exact-day matches;afterToday/beforeTodayare strict (today itself is rejected — compose withisTodayviaVUnionfor "today or later" / "today or earlier"). New codesVDateCode.isToday/sameDay/afterToday/beforeTodaywith default templates'Must be today'/'Must be the same day as {date}'/'Must be after today'/'Must be before today'. Pinned bytest/src/types/date_test.dart(4 new groups) and theVDateCoderegression intest/src/messages/messages_test.dart.V.string().treatEmptyAsNull({bool enabled = true})+V.treatEmptyAsNull(bool)(global setter). Normalizes""(string vazia exata, sem trim) paranullantes do pipeline da string, deixandonullable()/defaultValue()tratarem como missing. Default global éfalse(opt-in, não-breaking). Setting local sobrepõe o global por instância — útil para opt-out em um campo específico quando o global está ativo. Roda antes de qualquer.preprocess()do user. Whitespace-only não é afetado (compõe com.trim()se quiser). Casos típicos: forms em que o input vazio significa "não preenchido" (alinhando comvaliform). Pinned portest/src/types/string_test.dart::treatEmptyAsNull(11 casos) e chain assertion emfluent_chain_test.dart. Convenção do projeto: tests que ligam o global devem resetarV.treatEmptyAsNull(false)emsetUp— mesma higiene do locale.VObject<T>.whenMatchesRaw((Map<String, dynamic>) => bool, dependsOn:, then:)— universal sibling ofwhenMatches, callback receives the raw map view of the input (rebuilt lazily from extractors in entity mode, passed through directly in raw mode). Use when the schema needs to support bothsafeParse(T)andsafeParseRaw(Map)(the typicalvaliformsetup). Same skip semantics + non-emptydependsOncontract aswhenMatches.VObject<T>.refineFieldRaw((Map<String, dynamic>) => bool, path:, {stage, dependsOn})— universal field-scoped refine; callback receives the map view. Mirror ofrefineFieldfor use cases that consume both entity and raw modes.stage: RefineStage.post(default) gates ondependsOn;stage: RefineStage.preruns unconditionally. New_ObjectRawFieldRulestorage on VObject; previously the slot was occupied by an entity-typedrefineFieldRawthat has been promoted to astage:parameter onrefineField(see Changed below).
Changed #
- Breaking — old
VObject.refineFieldRaw((T) => bool, path:)removed; same behavior now expressed asrefineField(check, path:, stage: RefineStage.pre). The slotrefineFieldRawis now occupied by a new Map-typed universal variant (see Added). Migration:obj.refineFieldRaw((t) => check(t), path: 'x')→obj.refineField((t) => check(t), path: 'x', stage: RefineStage.pre). Same forVMap.refineFieldRaw— removed and replaced byrefineField((m) => ..., path:, stage: RefineStage.pre). The pre-pipeline timing semantics are preserved; only the entry point name and the newstage:parameter are different. Pinned bytest/src/features_test.dart::refineField stages (post vs pre). - Breaking —
whenMatches(onVMapandVObject) now skips when a declareddependsOnfield failed per-field validation. Previously the predicate was always called even when its declared dependencies had failed, leaving the callback responsible for defending against partial / wrong-typed input. The rule now follows the same gating asrefine(dependsOn:): if any key independsOnshows up infailedFieldPaths, the predicate is not called and the entirethenblock is not applied — protects the callback from casting a value that never passed its own validator. Side effect:dependsOnmust now be non-empty (anAssertionErrorfires at construction otherwise). Use plainrefine()/add()for rules that should always run regardless of field outcomes. Pinned by new tests intest/src/types/{map,object,object_raw}_test.dart. - Breaking —
refineFieldonVMapandVObjectnow unionsdependsOnwith{path}and rejects an empty set. Previously, passingdependsOn: {a, b}replaced the implicit{path}, which let callers accidentally drop their own path from the skip set and reach a callback whosepath-typed field had failed its own validator (cast crashes inVMap; logically-stale reads inVObject). The path is now always part of the effective deps; you only declare the extra fields the callback reads.dependsOn: const {}triggers anAssertionErrorat construction — a refineField that should run regardless of field failures is arefineField(stage: RefineStage.pre)(raw input, runs before per-field) or a plainrefine(dependsOn: const {})(entity-level, runs after per-field). Migration: if your code useddependsOn: {path, extraA, extraB}, you can simplify todependsOn: {extraA, extraB}. Pinned bytest/src/features_test.dart::refineField — dependsOn override.
Fixed #
whenMatchesRaw(entity mode) was using a stalefailedFieldPathssnapshot. The set of failed fields was computed before the_whenMatchesRulesloop and reused for the subsequent_whenMatchesRawRulesloop — meaning awhenMatchesRawrule whosedependsOnreferenced a field that failed via an earlierwhenMatches.thenwould not skip as documented. Now recomputed via_firstSegments(errors)between the two loops in bothsafeParseandsafeParseAsync. Pinned bytest/src/types/object_test.dart::whenMatchesRaw — entity mode::skips when whenMatches.then fails a declared dep field.
2.2.0 - 2026-05-08 #
Added #
VMap.partial({except})andVObject<T>.partial({except})— opt-in exception list on the existingpartial()method. With no argument the behavior is unchanged (every field becomes nullable). Passingexcept: ['id']keeps the listed keys with their original validator while every other field gets wrapped to acceptnull— the canonical pattern for an update DTO that retains a required identifier (id,slug, etc.). Keys must be declared on the schema; otherwise anAssertionErrorfires in debug, mirroring the assert behavior ofrefine(dependsOn:). Pipeline state (entity-level rules,when/whenMatches, preprocessors,nullable,defaultValue) is preserved exactly like the no-argument variant.
Fixed #
partial()now mirrors.nullable()semantics for fields withdefaultValueor innernullable. The internal_NullableWrapperpreviously short-circuited onnullinput and returnedVSuccess(null)without consulting the inner schema's null handling, so anydefaultValuedeclared on a wrapped field was silently dropped —V.map({'k': V.string().defaultValue('x')}).partial().parse({'k': null})produced{'k': null}instead of the{'k': 'x'}you would get from manually applying.nullable(). The wrapper now delegates to the inner schema whenever it has its own null handling (_hasDefaultor_isNullable), so default substitution and the inner nullable short-circuit run as documented; only when the inner is strict does the wrapper itself absorb the null.hasAsyncnow also delegates to the inner so async pipelines on a partial-wrapped field are detected by sync consumers (safeParsethrowsVAsyncRequiredException). Pinned by new tests underpartialintest/src/types/{map,object}_test.dart.
2.1.0 - 2026-05-01 #
Changed #
V.string().url()acceptsschemes: const {}(scheme-optional mode) andhostOnly: true(reject path/query/fragment). Default behavior is unchanged:schemes: nullkeeps the existing{http, https}-required validation. Passingschemes: const {}makes the scheme optional — bare hosts (google.com,www.google.com,localhost:8080) and any well-formedscheme://host(https://x.com,ftp://x.com) both pass. Independently,hostOnly: truerejects anything past the host:port, useful for "domain field" inputs that should never accept a path. Host validation also tightened across all modes: standard labels separated by dots with a TLD of 2+ alphabetic characters, OR the literallocalhost, plus an optional:port. The previous regex ([^\\s/$.?#].[^\\s]*) accepted hosts like_under_score.xand-leading.comthat no DNS resolver would accept; those now correctly fail.
Added #
V.string().domain()— shortcut for "host only, no scheme, no path". Accepts bare domains (google.com,www.example.com,api.v2.example.co.uk),localhost, and optional:port; rejects scheme prefixes (https://google.com), path / query / fragment (google.com/foo,google.com?x=1), and malformed hosts. Emits its own error codeVStringCode.domain('string.domain'→'Invalid domain') so the message matches the user's intent — for full URLs, keep using.url(...). Implemented asDomainValidatordelegating toUrlValidator(schemes: const {}, hostOnly: true)plus an early-return that rejects any input containing://.VArray<T>.distinct(by:)— uniqueness by an extracted key. Use it for arrays ofMap/ class instances where==on the elements themselves is meaningless. Thebyextractor is required (without one, use.unique()); duplicates are detected by collectingby(elem)into aSet. Emits the same error code asunique()(array.unique) since the failure mode is identical. Inspired bylodash.uniqByand SQLDISTINCT.VObject<T>.partial()— mirrorsVMap.partial(). Returns a newVObject<T>where every declared field's validator is wrapped sonullis accepted in addition to the original shape; non-null inputs still run the original validator. Preserves all pipeline state (entity-level rules,when/whenMatches, preprocessors,nullable,defaultValue). Useful for partial-update DTOs where the type still has every field but only a subset is required at the API boundary, removing the need to sprinkle.nullable()on everyfield(...)call. The README note that previously saidpartial()was unavailable onVObjectwas rewritten —strict()andpassthrough()remain unavailable (they assume a runtime-shapeable container, whichVObject<T>is not).VObject<T>.fieldIf(condition, name, extractor, validator)— same asfield(...)whenconditionistrue, no-op otherwise. Lets the fluent chain stay unbroken when declaring a field is conditional on a flag known at the call site (feature toggle, request context, partial-update DTO). VMap doesn't need an equivalent because Dart map literals already acceptif (cond) 'key': validator.VMap.whenMatches(condition, dependsOn:, then:)andVObject<T>.whenMatches(condition, dependsOn:, then:)— predicate-based conditional validation, sibling of the existingwhen(field, equals:, then:)(which is left untouched). Use it when a single literalequalsis not enough: the trigger depends on a comparison other than==(>,oneOf, ...) or on the combined value of multiple fields. The predicate receives the raw input (Map<String, dynamic>forVMap,TforVObject<T>) and runs at the same pipeline step aswhen— failures insidethencontribute tofailedFieldPathsand gate laterrefine(dependsOn:)rules.dependsOnis required (forces explicit declaration of the fields the predicate reads, so_knownKeys()can validate them and downstreamrefine(dependsOn:)keeps working). The predicate itself is always synchronous; validators insidethenmay be sync or async — awhenMatcheswhosethencontains an async validator opts the schema into async mode (hasAsync == true). VObject is strict (every key independsOnandthenmust already be declared via.field(...)); VMap follows the same liberal rule aswhenforthenkeys (they enter_knownKeysautomatically, so subsequentrefine(dependsOn: {<thatKey>})works without further setup). Read-only snapshot exposed viawhenMatchesRulesgetter on both types. Pinned bypipeline_contract_test.dart::whenMatches contract.applyIf(condition, builder)extension on everyVType. Conditional schema construction at build time (not at validation time): whenconditionistrue, returnsbuilder(this); otherwise returns the receiver unchanged. The generic<V extends VType>preserves the receiver's concrete type, so the fluent chain keeps working (V.string().applyIf(...).email()still returnsVString). Designed for parametrizing schemas from external flags (a feature toggle, a request context) without duplicating the schema body. For an else-branch, chain a secondapplyIfwith the negated condition. Lives inlib/src/extensions/apply_if.dart, exported frompackage:validart/validart.dart.
2.0.0 - 2026-04-25 #
Fixed #
preprocess(...)/preprocessAsync(...)now actually run on every container type. All container schemas —VMap,VArray,VObject,VEnum,VLiteral,VUnion,VTransformed(and its async counterpartVTransformedAsync) — previously overrodesafeParse/safeParseAsyncwithout invoking the base-class preprocess loop, silently dropping any.preprocess(fn)/.preprocessAsync(fn)registered on them. Only primitives (VString,VInt,VDouble,VBool,VDate) ever honored preprocessors. Every container now applies sync and async preprocessors before_resolveNull, soV.map({...}).preprocess((raw) => normalize(raw))and every analogous call transforms the input exactly like it does on primitives. Regression tests were added intest/src/types/{map,array,enum,literal,union,object}_test.dartandtest/src/features_test.dartso the bug cannot return silently.refine(...)now runs onVLiteral,VUnionandVTransformed/VTransformedAsync. Each of these overrodesafeParseand returnedVSuccess(value)directly on match instead of routing through_runPipeline(value), so any.refine(fn)/.refineAsync(fn)step was silently dropped. The overrides now delegate the final success path to_runPipeline(or_runPipelineAsync), which also makesadd()custom validators and chained transforms downstream of the fix work as documented.VTransformed<I, O>andVTransformedAsync<I, O>honor their ownnullable()/defaultValue(O). Previously the wrappers only consulted the inner schema's null-handling, soV.string().transform<int>((s) => s.length).defaultValue(0).parse(null)failed withstring.invalid_typebecause0was forwarded to the innerVString. The wrappers now short-circuit on null input when_hasDefaultor_isNullableis set on the wrapper itself — default goes through_runPipelineas the transformed outputO, and nullable returnsVSuccess<O?>(null)directly. Inner-nullable behavior (.nullable().transformAsync(...)) is preserved because the wrapper still delegates when it has no own null-handling set.- Pipeline contract test suite (
test/src/pipeline_contract_test.dart). Iterates every concreteVTypesubclass and asserts thatpreprocess,preprocessAsync,refine,refineAsync,nullable(),defaultValue()and the combination of all of them behave correctly. Adding a newVTypesubclass now requires one call to_runPipelineContract<T>(...)— a subclass that forgets to run the base-class preprocess loop (or to route success through_runPipeline) fails the contract immediately. VMap.partial()now preserves pipeline state from the base schema. It previously built a freshVMap(partialSchema)from scratch, silently dropping anyrefine/equalFields/refineField/add/when/strict/passthrough/nullable/defaultValue/ preprocessor declared before the call — diverging fromextend/merge/pick/omit, which all carry those over via_copyMapStateTo.partial()now does the same, so a base schema's entity-level rules survive the conversion to a patch schema. Pinned bytest/src/integration_scenarios_test.dart::partial() + dependsOn.- Entity-level validators with declared field dependencies now run even when unrelated fields fail.
equalFields(a, b)(declares{a, b}) andrefineField(check, path: 'x')(declares{x}) previously short-circuited if any field had errors; they now run as long as their declared dependencies passed, and their result is aggregated alongside field errors in a singleVFailure. Genericrefine(check)withoutdependsOnkeeps the conservative behavior — it still skips when any field error exists, since it lacks the metadata to know which fields it depends on.VArray.refineis unchanged (VArraydoes not have entity-level dependencies).whenrules continue to be evaluated as part of field-level validation. VEnum.safeParseandVLiteral.safeParsenow throwVAsyncRequiredExceptionwhen the schema has any async step. Previously, both overrides skipped thehasAsyncgate that every other container honors, so callingvalidate/parse(sync) onV.enm(Color.values).refineAsync(...)orV.literal('admin').preprocessAsync(...)silently ran only the sync portion of the pipeline —_runPipelinedoes not iterate_AsyncValidatorStep, so the async refine was dropped on the floor and the schema returnedtrue/VSuccessregardless. The gate now fires consistently acrossVString/VInt/VDouble/VBool/VDate/VArray/VMap/VObject/VUnion/VTransformed/VEnum/VLiteral, and the newpipeline_contract_test.dartcases forrunPreprocessorsexercise the path on every concreteVType.
Added #
VFailure.rootMessages()— returns the messages of every error with an emptypath(root-level errors, typically emitted byrefine/equalFieldsapplied directly on a schema root).toMap()is field-keyed and intentionally excludes those errors so they cannot leak into a UI's per-field display under a fake key;rootMessages()is the explicit channel for retrieving them as aList<String>to render in a form-wide banner. The two methods partition the errors cleanly: every error appears in exactly one oftoMap()orrootMessages(). Documented in the README under Form Errors → Root-level errors viarootMessages().refine(check, dependsOn: {'a', 'b'})onVMapandVObject. Optional parameter that declares which schema field keys this refine depends on. When provided, the refine skips only if one of those specific keys failed validation, and runs (aggregating its error with the field errors) otherwise. WithoutdependsOn, the existing conservative behavior is preserved (skip on any field error).dependsOnaccepts any field declared in the base schema or in anywhen.thenblock; anAssertionErroris thrown for unknown keys. Same parameter onrefineAsync. Built-inequalFieldsandrefineFielduse this internally to declare their dependencies ({a, b}and{path}respectively), so they automatically aggregate alongside unrelated field errors. The internaladd(validator, dependsOn:)parameter is also exposed so external packages can plug in custom entity-level validators with declared deps.VObject<T>.array()— fluent helper that returnsVArray<T>, mirroringVMap.array(). EnablesSignInDto.schema.array().min(1).unique()forList<T>validation without breaking the chain.VObject<T>.equalFields(fieldA, fieldB, {message})— cross-field equality check, mirroringVMap.equalFields. Canonical use case is DTO password confirmation. Emits the new error codeVObjectCode.fieldsNotEqual('object.fields_not_equal') with default template'{field} must be equal to {other}', customizable per-call viamessage:or globally viaVLocale({'object.fields_not_equal': '...'}). ThrowsAssertionErrorif either field name is not declared on the schema (uniform across all entity-level rules:equalFields,refineField,refine(dependsOn:)). Backed by a new internalObjectEqualFieldsValidator<T>.VObject<T>.when(field, equals:, then:)— conditional validation, mirroringVMap.when. When the value read fromfieldequalsequals, every validator inthenis applied to the corresponding field in addition to its baseline validator. Asserts thatfieldand every key inthenare declared on the schema. Propagates throughhasAsyncandsafeParseAsyncwhen anythenvalidator is async.VObject<T>.refineField(check, path:, message:)— entity-level predicate scoped to a specific field path. Thecheckreceives the wholeTinstance (so it can compare across fields), and the emitted error'spathis[path]. MirrorsVMap.refineFieldbut without the string-keyed lookup.VObject<T>.pick(List<String>)/.omit(List<String>)— derive a new schema containing only / excluding the specified fields. Preserves all pipeline state (validator steps,whenrules, preprocessors,nullable,defaultValue). Unlike TypeScript, the input type staysT— only the validation surface is narrowed; this is not a subset type. For partial/dynamic payloads, useVMap.partial()instead.VObject<T>.merge(VObject<T> other)— combine two schemas of the sameTinto a new one. Fields and pipeline state (validator steps,whenrules, preprocessors,nullable,defaultValue) from both sides are concatenated/OR-ed. Useful for composing sharedaudit/metafield groups with domain-specific schemas.VType.runPreprocessors(value)/runPreprocessorsAsync(value)— public accessors to the preprocess stage of any schema, returning the value after the preprocess chain ran (sync chain only forrunPreprocessors; sync + async, in registration order, for the async variant). Does NOT run_resolveNull, validators, or transforms — only the preprocess stage. Sync variant throwsVAsyncRequiredExceptionwhen the schema has any async preprocessor. Useful for downstream consumers (e.g.valiform) that need to mirror the container preprocess in their own scoped pipeline before running per-field checks. Every concrete container (VType,VMap,VObject,VArray,VUnion,VTransformed,VTransformedAsync,VEnum,VLiteral) was refactored to use these methods internally for theirsafeParse/safeParseAsyncpreprocess stage — behaviour is identical to before, only the duplicated loop is gone. Pinned bytest/src/pipeline_contract_test.dart, which now asserts both methods on every concreteVType.VType.hasPreprocessors—truewhen the schema has at least one preprocessor registered (sync or async). Lets consumers gate the preprocess plumbing on actually-needed work;valiform's per-field validator uses it to skip snapshot construction entirely on schemas without container preprocess.VMap.refineFieldRaw(check, {path, message})andVObject<T>.refineFieldRaw(check, {path, message})— entity-level rules scoped to a field path that run before any per-field iteration. The callback receives the raw input (Map<String, dynamic>forVMap,TforVObject<T>) — each field still as it arrived from the input, no field-level preprocess / validators / transforms applied yet. Compare withrefineField, whose callback runs after the full per-field pipeline (and whosedependsOnis implicitly{path});refineFieldRawhas nodependsOnbecause no field has been validated yet, so it always runs once the type check succeeds. Error path is[path](mirrorsrefineField) so consumers likevaliform'sVFormsurface it inline under the target field. UserefineFieldRawwhen the rule depends on the input as the user typed it (raw casing, whitespace, pre-coercion shape); reach forrefineFieldfor everything else. Backed by a new_RawValidatorStep<T>and a publicVType.addRaw(...)low-level API. Documented in README → Cross-Field Validation.- Factory-level
invalidTypeMessage:parameter. Every factory (V.string(),V.int(),V.bool(),V.date(),V.map(),V.array(),V.object(),V.enm(),V.literal(),V.union()) now accepts an optionalinvalidTypeMessage:that customizes theinvalid_typeerror fired when input is non-null but has the wrong runtime type (e.g.42againstV.string()). It coexists with the existingmessage:(which still scopes only to therequirederror onnullinput) — set one, the other, or both. Mirrors Zod'srequired_error/invalid_type_errorseparation;requiredis typically a user-facing label,invalid_typeis a developer-facing signal, so keeping the channels separate avoids one polluting the other. The error code is unchanged (string.invalid_type,int.invalid_type, ...); only the message text is replaced when set. WithoutinvalidTypeMessage:, the locale template'Expected {expected}, received {received}'is preserved verbatim.VEnumandVLiteralaccept the parameter for API uniformity but the override never fires there — they emitenum.invalid/literal.invalidcodes instead ofinvalid_type.
Changed #
- Breaking —
VObjectnow uses a fluent.field()API instead of theconfigure:callback builder. TheVObjectBuilder<T>class and theconfigure:named parameter on bothV.object<T>(...)andVObject<T>(...)were removed. Field extractors are now chained directly on the schema with the same.field(name, extractor, validator)signature, identical to the rest of the library (V.string().email().min(5)). Before:V.object<User>(configure: (o) => o.field('name', (u) => u.name, V.string()).field('age', (u) => u.age, V.int()));After:V.object<User>().field('name', (u) => u.name, V.string()).field('age', (u) => u.age, V.int());. Migration: dropconfigure: (o) =>and theo.prefix — the rest of the chain is unchanged. Enables thestatic finalDTO pattern:class SignInDto { static final schema = V.object<SignInDto>().field(...).field(...); }, so the schema is built once per isolate and reused without re-wrapping it inV.object<X>(...)at every call site.
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())
0.1.0 - 2025-02-23 #
Added #
ValidationModesupport for:.cep().cnpj().cpf().phone()
- Now, it is possible to validate formatted strings (
ValidationMode.formatted) or unformatted ones (ValidationMode.unformatted). - Improved documentation for CPF, CNPJ, CEP, and Phone, explaining how to use
ValidationMode.
Changed #
- Updated API to support both formatted and unformatted validation for documents and phone numbers.
0.0.4 - 2025-02-20 #
Added #
- Implemented an assertion in
VMap.refine()to ensure the providedpathexists in the defined object schema.- This prevents referencing non-existent fields, improving validation reliability.
- Added a test case to verify that an
AssertionErroris thrown when an invalid path is used.
Changed #
- Updated the README with more detailed documentation and examples.
- Improved API documentation to ensure full coverage across all public elements.
0.0.3 - 2025-02-19 #
Added #
- Introduced a new primitive validator:
v.date(), allowing date-based validations. - Implemented
.prime()validator for integers to check if a number is prime. - Added new string validators:
password(): Ensures password complexity.jwt(): Validates JSON Web Tokens.card(): Validates credit card numbers.integer(): Ensures the string represents a valid integer.double(): Ensures the string represents a valid double.slug(): Ensures the string is a valid slug format (lowercase, hyphens, no spaces or special characters).alpha(): Ensures the string contains only alphabetic characters.alphanumeric(): Ensures the string contains only letters and numbers.
- Expanded test suite, achieving 100% test coverage.
Changed #
- Standardized class names for greater consistency across the library.
- Improved error messages and default validation messages.
- Adjusted validation logic for maps within arrays, fixing issues with nested validations.
- Updated README.md to include detailed documentation on array validations, including
.array(),.unique(),.contains(),.min(),.max(), and other related methods. - Improved examples for string and integer array validation.
Fixed #
- Resolved bugs related to map validation within arrays, ensuring proper validation behavior.
0.0.2 - 2025-02-17 #
Changed #
- Renamed
string().startsWidthtostring().startsWithfor consistency. - Updated README to reflect recent changes and improvements.
Removed #
- Removed
.any()and.every()functions from.map()as they were not applicable.
Added #
- Achieved 100% test coverage, ensuring full validation reliability.
- Added default messages for all validation types, providing a consistent error handling experience.
0.0.1 - 2025-02-16 #
- Initial release