validart 1.2.0 copy "validart: ^1.2.0" to clipboard
validart: ^1.2.0 copied to clipboard

A type-safe validation library for Dart, inspired by Zod. Supports parse/safeParse, transforms, coercion, schema composition, and structured errors.

example/example.dart

import 'package:validart/validart.dart';

Future<void> main() async {
  stringExamples();
  intExamples();
  doubleExamples();
  boolExamples();
  dateExamples();
  arrayExamples();
  mapExamples();
  objectExamples();
  enumExamples();
  literalExamples();
  unionExamples();
  coercionExamples();
  coreExamples();
  await asyncExamples();
  localeExamples();
}

void stringExamples() {
  print('--- string ---');

  // notEmpty
  print(V.string().notEmpty().validate('hello')); // true
  print(V.string().notEmpty().validate('')); // false

  // min / max / length
  print(V.string().min(3).validate('abc')); // true
  print(V.string().max(5).validate('hello')); // true
  print(V.string().length(4).validate('test')); // true

  // email / url / uuid / ip
  print(V.string().email().validate('user@example.com')); // true
  print(V.string().url().validate('https://example.com')); // true
  print(V.string().url().validate('ftp://example.com')); // false (default)
  print(
    V
        .string()
        .url(schemes: {'http', 'https', 'ftp'}).validate('ftp://example.com'),
  ); // true
  print(V.string().uuid().validate('550e8400-e29b-41d4-a716-446655440000'));
  print(V.string().ip().validate('192.168.1.1')); // true

  // pattern
  print(V.string().pattern(r'^\d{3}$').validate('123')); // true

  // date (multi-format default) / date strict format
  print(V.string().date().validate('2024-01-15')); // true (ISO)
  print(V.string().date().validate('15/01/2024')); // true (BR)
  print(V.string().date(format: 'DD/MM/YYYY').validate('15/01/2024')); // true
  print(V.string().date(format: 'DD/MM/YYYY').validate('2024-01-15')); // false

  // time
  print(V.string().time().validate('14:30')); // true
  print(V.string().time().validate('14:30:59')); // true

  // contains / startsWith / endsWith / equals
  print(V.string().contains('world').validate('hello world')); // true
  print(V.string().startsWith('hello').validate('hello world')); // true
  print(V.string().endsWith('.dart').validate('main.dart')); // true
  print(V.string().equals('exact').validate('exact')); // true

  // alpha / alphanumeric / slug
  print(V.string().alpha().validate('abcXYZ')); // true
  print(V.string().alphanumeric().validate('abc123')); // true
  print(V.string().slug().validate('hello-world')); // true

  // password / jwt
  print(V.string().password().validate('Str0ng!Pass')); // true
  print(V
      .string()
      .password()
      .validate('Str0ng_Pass')); // false ('_' not in default)
  print(
    V
        .string()
        .password(specialChars: r'!@#$%^&*()-_+=<>?')
        .validate('Str0ng_Pass'),
  ); // true
  print(V.string().jwt().validate('eyJh.eyJz.SflKx')); // true

  // card (default — any Luhn-valid)
  print(V.string().card().validate('4532015112830366')); // true
  print(V.string().card().validate('4111 1111 1111 1111')); // true (masked)

  // card (restricted to specific brands)
  final visaOrMaster = V.string().card(
    brands: [const VisaBrand(), const MastercardBrand()],
  );
  print(visaOrMaster.validate('4111111111111111')); // true (Visa)
  print(visaOrMaster.validate('5555555555554444')); // true (Mastercard)
  print(visaOrMaster.validate('378282246310005')); // false (Amex)

  // card (pin input shape via ValidationMode)
  final formattedCard = V.string().card(mode: ValidationMode.formatted);
  print(formattedCard.validate('4532 0151 1283 0366')); // true
  print(formattedCard.validate('4532015112830366')); // false (missing groups)

  final unformattedCard = V.string().card(mode: ValidationMode.unformatted);
  print(unformattedCard.validate('4532015112830366')); // true
  print(unformattedCard.validate('4532 0151 1283 0366')); // false

  // phone (default E.164 — `+` optional)
  print(V.string().phone().validate('+14155552671')); // true
  print(V.string().phone().validate('14155552671')); // true

  // phone (require or forbid the country-code prefix)
  final requiredPrefix = V.string().phone(
        pattern: const E164PhonePattern(
          countryCode: CountryCodeFormat.required,
        ),
      );
  print(requiredPrefix.validate('+14155552671')); // true
  print(requiredPrefix.validate('14155552671')); // false

  final noPrefix = V.string().phone(
        pattern: const E164PhonePattern(countryCode: CountryCodeFormat.none),
      );
  print(noPrefix.validate('14155552671')); // true
  print(noPrefix.validate('+14155552671')); // false

  // phone (custom pattern)
  final localPhone = V.string().phone(pattern: const LocalPhonePattern());
  print(localPhone.validate('LOCAL:1234')); // true
  print(localPhone.validate('+5511999999999')); // false

  // cvv / base64 / hex color / mac / semver / mongoId / iban / json
  print(V.string().cvv().validate('123')); // true
  print(V.string().base64().validate('SGVsbG8=')); // true
  print(V.string().hexColor().validate('#FF0000')); // true
  print(V.string().mac().validate('AA:BB:CC:DD:EE:FF')); // true
  print(V.string().semver().validate('1.2.3-alpha+build')); // true
  print(V.string().mongoId().validate('507f1f77bcf86cd799439011')); // true
  print(V.string().iban().validate('GB82 WEST 1234 5698 7654 32')); // true
  print(V.string().json().validate('{"a":1}')); // true

  // integer / numeric — validate string is parseable as int/double
  // (keeps output as String; use V.coerce.int()/double() to convert)
  print(V.string().integer().validate('42')); // true
  print(V.string().integer().validate('-42')); // true
  print(V.string().integer().validate('3.14')); // false
  print(V.string().numeric().validate('3.14')); // true
  print(V.string().numeric().validate('42e3')); // true
  print(V.string().numeric().validate('NaN')); // false
  print(V.string().numeric().validate('Infinity')); // false

  // ULID / NanoID
  print(V.string().ulid().validate('01ARZ3NDEKTSV4RRFFQ69G5FAV')); // true
  print(V.string().nanoId().validate('V1StGXR8_Z5jdHi6B-myT')); // true
  print(V.string().nanoId(length: 10).validate('V1StGXR8_Z')); // true

  // UUID version filter — v7 is timestamp-ordered (popular in modern APIs)
  print(
    V
        .string()
        .uuid(version: UuidVersion.v7)
        .validate('018fcb2e-ea3f-7a3d-b91e-8f2e0c9b33d9'),
  ); // true

  // postal code (pluggable — built-ins US, CA, UK)
  print(
    V.string().postalCode(pattern: const UsZipPattern()).validate('94103'),
  ); // true
  print(
    V
        .string()
        .postalCode(pattern: const CaPostalCodePattern())
        .validate('K1A 0B1'),
  ); // true
  print(
    V
        .string()
        .postalCode(pattern: const UkPostcodePattern())
        .validate('SW1A 1AA'),
  ); // true

  // postal code with ValidationMode — require or forbid the separator
  print(
    V
        .string()
        .postalCode(
          pattern: const UkPostcodePattern(mode: ValidationMode.formatted),
        )
        .validate('SW1A1AA'),
  ); // false (no space)
  print(
    V
        .string()
        .postalCode(
          pattern: const CaPostalCodePattern(mode: ValidationMode.unformatted),
        )
        .validate('K1A0B1'),
  ); // true

  // tax ID built-ins
  print(
    V.string().taxId(pattern: const UsSsnPattern()).validate('123-45-6789'),
  ); // true
  print(
    V.string().taxId(pattern: const UkNiNumberPattern()).validate('AB123456C'),
  ); // true
  print(
    V.string().taxId(pattern: const CaSinPattern()).validate('046-454-286'),
  ); // true

  // tax ID with ValidationMode — pin formatted vs unformatted
  print(
    V
        .string()
        .taxId(pattern: const UsSsnPattern(mode: ValidationMode.formatted))
        .validate('123456789'),
  ); // false (dashes required)
  print(
    V
        .string()
        .taxId(pattern: const CaSinPattern(mode: ValidationMode.unformatted))
        .validate('130692544'),
  ); // true

  // license plate built-in (UK post-2001)
  print(
    V
        .string()
        .licensePlate(pattern: const UkPlatePattern())
        .validate('AB12 CDE'),
  ); // true

  // license plate with ValidationMode
  print(
    V
        .string()
        .licensePlate(
          pattern: const UkPlatePattern(mode: ValidationMode.unformatted),
        )
        .validate('AB12CDE'),
  ); // true

  // Extend with your own patterns for country-specific needs:
  print(
    V.string().taxId(pattern: const DummyTaxIdPattern()).validate('TAX:123'),
  ); // true
  print(
    V
        .string()
        .licensePlate(pattern: const DummyPlatePattern())
        .validate('ABC-1234'),
  ); // true

  // transforms (pre-processing — always run before validators)
  print(V.string().trim().parse('  hello  ')); // 'hello'
  print(V.string().toLowerCase().parse('HELLO')); // 'hello'
  print(V.string().toUpperCase().parse('hello')); // 'HELLO'
  print(V.string().toPascalCase().parse('hello world')); // 'HelloWorld'
  print(V.string().toCamelCase().parse('hello_world')); // 'helloWorld'
  print(V.string().toSnakeCase().parse('HelloWorld')); // 'hello_world'
  print(V.string().toScreamingSnakeCase().parse('helloWorld')); // 'HELLO_WORLD'
  print(V.string().toSlug().parse('My Blog Post!')); // 'my-blog-post'

  // accents — stripped by default, preserved with keepAccents: true
  print(V.string().toSlug().parse('São João')); // 'sao-joao'
  print(V.string().toSlug(keepAccents: true).parse('São João')); // 'são-joão'
  print(V.string().toPascalCase().parse('maçã fresca')); // 'MacaFresca'
}

void intExamples() {
  print('--- int ---');

  print(V.int().min(5).validate(10)); // true
  print(V.int().max(10).validate(5)); // true
  print(V.int().positive().validate(1)); // true
  print(V.int().negative().validate(-1)); // true
  print(V.int().between(1, 10).validate(5)); // true
  print(V.int().multipleOf(3).validate(9)); // true
  print(V.int().even().validate(4)); // true
  print(V.int().odd().validate(3)); // true
  print(V.int().prime().validate(7)); // true

  // array of ints
  print(V.int().positive().array().validate([1, 2, 3])); // true
}

void doubleExamples() {
  print('--- double ---');

  print(V.double().min(1.5).validate(2.0)); // true
  print(V.double().max(9.9).validate(5.0)); // true
  print(V.double().positive().validate(0.1)); // true
  print(V.double().negative().validate(-0.1)); // true
  print(V.double().between(1.0, 10.0).validate(5.5)); // true
  print(V.double().multipleOf(0.5).validate(1.5)); // true
  print(V.double().finite().validate(3.14)); // true
  print(V.double().finite().validate(double.infinity)); // false
  print(V.double().decimal().validate(3.14)); // true (has fractional part)
  print(V.double().integer().validate(3.0)); // true (whole number)

  // array of doubles
  print(V.double().finite().array().validate([1.0, 2.5])); // true
}

void boolExamples() {
  print('--- bool ---');

  print(V.bool().isTrue().validate(true)); // true
  print(V.bool().isTrue().validate(false)); // false
  print(V.bool().isFalse().validate(false)); // true

  // array of bools
  print(V.bool().array().validate([true, false])); // true
}

void dateExamples() {
  print('--- date ---');

  final ref = DateTime(2024, 1, 1);

  print(V.date().after(ref).validate(DateTime(2024, 6, 15))); // true
  print(V.date().before(ref).validate(DateTime(2023, 6, 15))); // true
  print(
    V
        .date()
        .between(ref, DateTime(2024, 12, 31))
        .validate(DateTime(2024, 6, 15)),
  ); // true

  // 2024-01-15 is Monday
  print(V.date().weekday().validate(DateTime(2024, 1, 15))); // true
  // 2024-01-06 is Saturday
  print(V.date().weekend().validate(DateTime(2024, 1, 6))); // true

  // age — computed against DateTime.now()
  final now = DateTime.now();
  final thirty = DateTime(now.year - 30, now.month, now.day);
  print(V.date().age(min: 18).validate(thirty)); // true
  print(V.date().age(min: 18, max: 65).validate(thirty)); // true
}

void arrayExamples() {
  print('--- array ---');

  // min / max
  print(V.string().array().min(1).validate(['a'])); // true
  print(V.string().array().max(3).validate(['a', 'b'])); // true

  // unique
  print(V.int().array().unique().validate([1, 2, 3])); // true
  print(V.int().array().unique().validate([1, 1, 2])); // false

  // contains
  print(V.int().array().contains([1, 2]).validate([1, 2, 3])); // true

  // nested arrays
  final matrix = VArray<List<int>>(V.int().array());
  print(
    matrix.validate([
      [1, 2],
      [3, 4],
    ]),
  ); // true

  // refine
  final sumLeq10 = V.int().array().refine(
        (v) => v.fold<int>(0, (a, b) => a + b) <= 10,
        message: 'Sum must be <= 10',
      );
  print(sumLeq10.validate([1, 2, 3])); // true
  print(sumLeq10.validate([5, 6])); // false
}

void mapExamples() {
  print('--- map ---');

  // basic
  final userSchema = V.map({
    'name': V.string().min(1),
    'email': V.string().email(),
    'age': V.int().min(0).nullable(),
  });
  print(
    userSchema.validate({'name': 'Alice', 'email': 'alice@ex.com', 'age': 30}),
  ); // true

  // pick / omit
  final picked = userSchema.pick(['name', 'email']);
  print(picked.validate({'name': 'Alice', 'email': 'alice@ex.com'})); // true

  final omitted = userSchema.omit(['age']);
  print(omitted.validate({'name': 'Alice', 'email': 'alice@ex.com'})); // true

  // extend / merge
  final extended = userSchema.extend({'password': V.string().min(8)});
  print(extended.validate({
    'name': 'Alice',
    'email': 'a@b.com',
    'password': '12345678',
  })); // true

  final schemaA = V.map({'a': V.string()});
  final schemaB = V.map({'b': V.int()});
  final merged = schemaA.merge(schemaB);
  print(merged.validate({'a': 'x', 'b': 1})); // true

  // partial (all fields become nullable)
  final partialSchema = V.map({'name': V.string()}).partial();
  print(partialSchema.validate({'name': null})); // true

  // strict (rejects extra keys)
  final strictSchema = V.map({'name': V.string()}).strict();
  print(strictSchema.validate({'name': 'Jo', 'extra': true})); // false

  // passthrough (keeps extra keys in parsed output)
  final passthrough = V.map({'name': V.string()}).passthrough();
  print(passthrough.parse({'name': 'Jo', 'extra': true}));
  // {name: Jo, extra: true}

  // equalFields
  final register = V.map({
    'password': V.string().min(8),
    'confirm': V.string(),
  }).equalFields('confirm', 'password');
  print(register.validate({
    'password': '12345678',
    'confirm': '12345678',
  })); // true

  // refineField (custom per-field validation)
  final ageCheck = V.map({
    'age': V.int(),
  }).refineField(
    (data) => (data['age'] as int) >= 18,
    path: 'age',
    message: 'Must be at least 18',
  );
  print(ageCheck.validate({'age': 20})); // true
  print(ageCheck.validate({'age': 10})); // false

  // when (conditional)
  final form = V.map({
    'type': V.string(),
    'cnpj': V.string().nullable(),
    'cpf': V.string().nullable(),
  }).when('type', equals: 'company', then: {'cnpj': V.string().min(14)}).when(
      'type',
      equals: 'person',
      then: {'cpf': V.string().min(11)});
  print(form.validate({'type': 'company', 'cnpj': '12345678901234'})); // true
  print(form.validate({'type': 'person', 'cpf': '12345678901'})); // true

  // array of maps
  final users = V.map({'name': V.string().min(1)}).array();
  print(users.validate([
    {'name': 'Alice'},
    {'name': 'Bob'},
  ])); // true
}

void objectExamples() {
  print('--- object ---');

  // field (type-safe extraction)
  final folderSchema = V.object<Folder>(
    configure: (o) => o
        .field('id', (f) => f.id, V.string().uuid())
        .field('name', (f) => f.name, V.string().min(1)),
  );
  final folder = Folder(
    id: '550e8400-e29b-41d4-a716-446655440000',
    name: 'Docs',
  );
  print(folderSchema.validate(folder)); // true

  // refine on entity
  final namedFolder = V.object<Folder>().refine(
        (f) => f.name.isNotEmpty,
        message: 'Name cannot be empty',
        code: 'empty_name',
      );
  print(namedFolder.validate(folder)); // true
}

void enumExamples() {
  print('--- enum ---');

  final colorSchema = V.enm(Color.values);
  print(colorSchema.validate(Color.red)); // true
  print(colorSchema.validate('red')); // false

  // array of enums
  print(colorSchema.array().validate([Color.red, Color.blue])); // true
}

void literalExamples() {
  print('--- literal ---');

  final adminSchema = V.literal('admin');
  print(adminSchema.validate('admin')); // true
  print(adminSchema.validate('user')); // false
}

void unionExamples() {
  print('--- union ---');

  // Accepts either a UUID string or a positive int
  final idSchema = V.union([V.string().uuid(), V.int().min(1)]);
  print(idSchema.validate('550e8400-e29b-41d4-a716-446655440000')); // true
  print(idSchema.validate(42)); // true
  print(idSchema.validate('bad')); // false
}

void coercionExamples() {
  print('--- coerce ---');

  print(V.coerce.int().parse('42')); // 42
  print(V.coerce.int().parse(3.7)); // 3
  print(V.coerce.double().parse('3.14')); // 3.14
  print(V.coerce.string().parse(42)); // '42'
  print(V.coerce.bool().parse('true')); // true
  print(V.coerce.bool().parse(0)); // false
  print(V.coerce.date().parse('2024-01-15')); // DateTime(2024, 1, 15)
}

void coreExamples() {
  print('--- core ---');

  // nullable
  print(V.string().nullable().parse(null)); // null

  // defaultValue
  print(V.string().defaultValue('fallback').parse(null)); // 'fallback'

  // message — custom per-schema message for null input (required error).
  final terms = V.bool(message: 'You must accept the terms').isTrue();
  print(terms.errors(null)?.first.message); // 'You must accept the terms'

  // Custom messages: factory-level `message` vs validator-level `message`.
  //
  // Same parameter name, different scopes:
  //   - Factory `V.string(message: ...)` → only fires on null input
  //     (pre-validation `required` error).
  //   - Validator `.email(message: ...)` / `.min(n, message: ...)` →
  //     only fires when that specific validator rejects a non-null value.
  // The two never overlap and can coexist on the same schema.

  final onlyRequired = V.string(message: 'Name is required').min(3);
  print(onlyRequired.errors(null)?.first.message); // 'Name is required'

  final onlyValidator = V.string().email(message: 'Invalid email format');
  print(onlyValidator.errors('bad')?.first.message); // 'Invalid email format'

  final both = V
      .string(message: 'Name is required')
      .min(3, message: (n) => 'At least $n chars');
  print(both.errors(null)?.first.message); // 'Name is required'
  print(both.errors('ab')?.first.message); // 'At least 3 chars'

  // refine (custom validation)
  final evenLen = V.string().refine(
        (v) => v.length.isEven,
        message: 'Length must be even',
        code: 'even_length',
      );
  print(evenLen.validate('abcd')); // true
  print(evenLen.validate('abc')); // false

  // transform<O> — change the output type
  final length = V.string().transform<int>((s) => s.length);
  print(length.parse('hello')); // 5

  // preprocess — transform before type check
  final trimmed = V.string().preprocess((v) => v?.toString().trim() ?? '');
  print(trimmed.parse(42)); // '42'

  // safeParse with pattern matching
  final result = V.string().email().safeParse('bad');
  switch (result) {
    case VSuccess(:final value):
      print('ok: $value');
    case VFailure(:final errors):
      print('errors: ${errors.map((e) => e.code).toList()}');
    // errors: [invalid_email]
  }

  // errors() + VFailure.toMap()
  final form = V.map({
    'email': V.string().email(),
    'name': V.string().min(3),
  }).safeParse({'email': 'bad', 'name': 'Al'});

  if (form case VFailure()) {
    print(form.toMap());
    // {email: Invalid email address, name: String must be at least 3 ...}
  }
}

Future<void> asyncExamples() async {
  print('--- async ---');

  // Simulate an async uniqueness check.
  Future<bool> isEmailAvailable(String email) async {
    await Future.delayed(const Duration(milliseconds: 10));
    return email != 'taken@example.com';
  }

  final schema = V.string().email().refineAsync(
        isEmailAvailable,
        message: 'Email already registered',
        code: 'email_taken',
      );

  print(await schema.validateAsync('new@example.com')); // true
  print(await schema.validateAsync('taken@example.com')); // false

  // Sync consumers on an async schema throw VAsyncRequiredException.
  try {
    schema.validate('x');
  } on VAsyncRequiredException catch (e) {
    print('caught: ${e.suggestion}'); // caught: validateAsync
  }

  // Sync-only schemas still work sync without any overhead.
  print(V.string().email().validate('a@b.com')); // true

  // addAsync — custom AsyncValidator (reusable across schemas)
  final usernameSchema = V.string().addAsync(const _UsernameAvailable());
  print(await usernameSchema.validateAsync('new_user')); // true
  print(await usernameSchema.validateAsync('taken')); // false

  // refineAsync with timeout
  final slowSchema = V.string().refineAsync(
    (v) async {
      await Future<void>.delayed(const Duration(milliseconds: 200));
      return true;
    },
    timeout: const Duration(milliseconds: 50),
    message: 'Timed out',
  );
  print(await slowSchema.validateAsync('x')); // false (timed out)

  // preprocessAsync — transform input before type check
  final resolvedSchema = V.string().preprocessAsync((raw) async {
    await Future<void>.delayed(const Duration(milliseconds: 1));
    return raw.toString().trim();
  }).min(3);
  print(await resolvedSchema.validateAsync('  hello  ')); // true

  // transformAsync — change output type asynchronously
  final lengthSchema = V.string().min(3).transformAsync<int>((v) async {
    await Future<void>.delayed(const Duration(milliseconds: 1));
    return v.length;
  });
  print(await lengthSchema.parseAsync('hello')); // 5
}

void localeExamples() {
  print('--- locale ---');

  V.setLocale(const VLocale({
    'invalid_email': 'Email inválido',
    'positive': 'Deve ser positivo',
    'required': 'Campo obrigatório',
  }));

  print(V.string().email().errors('bad')?.first.message);
  // 'Email inválido'

  print(V.int().positive().errors(-1)?.first.message);
  // 'Deve ser positivo'

  // Type-specific overrides — flat and nested forms coexist.
  // Emitted codes are prefixed (e.g. 'string.required', 'int.required').
  V.setLocale(const VLocale({
    // Generic fallback.
    'required': 'Campo obrigatório',

    // Nested — affects VString only.
    'string': {'required': 'Texto obrigatório'},

    // Flat — affects VInt only.
    'int.required': 'Número obrigatório',
  }));

  print(V.string().errors(null)?.first.message); // 'Texto obrigatório'
  print(V.int().errors(null)?.first.message); // 'Número obrigatório'
  print(V.bool().errors(null)?.first.message); // 'Campo obrigatório' (fallback)

  // Reset to default
  V.setLocale(const VLocale());
}

class Folder {
  final String id;
  final String name;
  Folder({required this.id, required this.name});
}

enum Color { red, green, blue }

class LocalPhonePattern extends PhonePattern {
  const LocalPhonePattern();

  @override
  String get code => 'invalid_phone_local';

  @override
  Map<String, dynamic>? validate(String value) =>
      value.startsWith('LOCAL:') ? null : {};
}

class DummyTaxIdPattern extends TaxIdPattern {
  const DummyTaxIdPattern();

  @override
  String get name => 'Dummy Tax ID';

  @override
  bool matches(String value) => value.startsWith('TAX:');
}

class _UsernameAvailable extends AsyncValidator<String> {
  const _UsernameAvailable();

  @override
  String get code => 'username_taken';

  @override
  Future<Map<String, dynamic>?> validate(String value) async {
    await Future<void>.delayed(const Duration(milliseconds: 1));
    return value == 'taken' ? {} : null;
  }
}

class DummyPlatePattern extends LicensePlatePattern {
  const DummyPlatePattern();

  @override
  String get name => 'Dummy Plate';

  @override
  bool matches(String value) => RegExp(r'^[A-Z]{3}-\d{4}$').hasMatch(value);
}
3
likes
0
points
575
downloads

Publisher

verified publisheredunatalec.com

Weekly Downloads

A type-safe validation library for Dart, inspired by Zod. Supports parse/safeParse, transforms, coercion, schema composition, and structured errors.

Repository (GitHub)
View/report issues

Topics

#validation #schema #parsing #form #i18n

License

unknown (license)

More

Packages that depend on validart