EzValidator
特徴
EzValidatorは、Flutter向けに設計された非常にシンプルなフィールドとオブジェクトスキーマ検証ライブラリです。Yupの直感的なAPIに触発され、Flutterアプリケーション内でデータスキーマの定義と実施を簡素化します。
主な特徴
- Flutterスキーマビルダー: Flutterプロジェクトにシームレスに統合し、データ検証スキーマを構築・管理
- 多用途な検証: 個別のフィールドや複雑なオブジェクト全体の検証に対応
- 型安全: ジェネリクスを使用した型安全な検証
- カスタムバリデーション:
addMethodを使用した柔軟なカスタム検証ルール - 国際化対応: カスタムロケールによるエラーメッセージのローカライズ
- 条件付き検証:
when、dependsOn、unionを使用した高度な検証ロジック - データ変換:
transformを使用した検証前のデータ前処理
インストール
dependencies:
ez_validator: ^0.3.6
基本的な使い方
スキーマの定義
final EzSchema userSchema = EzSchema.shape({
"email": EzValidator<String>(label: "メールアドレス")
.required()
.email(),
"password": EzValidator<String>(label: 'パスワード')
.required()
.minLength(8),
'date': EzValidator<DateTime>()
.required()
.date()
.minDate(DateTime(2019))
.maxDate(DateTime(2025)),
});
データの検証
// エラーのみを取得
final errors = userSchema.catchErrors({
'email': '[email protected]',
'password': '12345678',
'date': DateTime.now(),
});
if (errors.isEmpty) {
print("検証成功!");
} else {
print("検証エラー: $errors");
}
// データとエラーを同時に取得
final (data, errors) = userSchema.validateSync({
'email': '[email protected]',
'password': '12345678',
'date': DateTime.now(),
});
print(data); // 処理されたデータ(デフォルト値を含む)
print(errors); // 検証エラー
Flutter ウィジェットでの使用
TextFormFieldでの例
class LoginForm extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
validator: EzValidator<String>()
.required("メールアドレスは必須です")
.email("有効なメールアドレスを入力してください")
.build(),
decoration: const InputDecoration(
labelText: 'メールアドレス',
hintText: '[email protected]',
),
),
TextFormField(
obscureText: true,
validator: EzValidator<String>()
.required("パスワードは必須です")
.minLength(8, "パスワードは8文字以上必要です")
.matches(
RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]'),
"大文字、小文字、数字、特殊文字を含む必要があります",
)
.build(),
decoration: const InputDecoration(
labelText: 'パスワード',
),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// フォームが有効な場合の処理
}
},
child: const Text('ログイン'),
),
],
),
);
}
}
高度な使い方
カスタムバリデーション
// JSON構造の検証
final checkJson = EzValidator<Map<String, dynamic>>()
.addMethod((v) => v?['foo'] == 'bar' ? null : 'fooはbarである必要があります')
.addMethod((v) => v?['bar'] == 'Flutter' ? null : 'barはFlutterである必要があります')
.addMethod((v) => v?['items'] is List && v!['items'].isNotEmpty
? null
: 'itemsは空でないリストである必要があります')
.build();
// カスタムパスワード検証
final passwordValidator = EzValidator<String>()
.required("パスワードは必須です")
.minLength(8, "8文字以上必要です")
.addMethod((password) {
if (password == null) return null;
// 一般的な弱いパスワードをチェック
final weakPasswords = ['password', '12345678', 'qwerty'];
if (weakPasswords.contains(password.toLowerCase())) {
return 'このパスワードは弱すぎます';
}
return null;
});
条件付き検証(when)
final EzSchema registrationSchema = EzSchema.shape({
"password": EzValidator<String>()
.required()
.minLength(8, "パスワードは8文字以上必要です"),
"confirmPassword": EzValidator<String>().when(
"password",
(confirmValue, [ref]) =>
confirmValue == ref?["password"]
? null
: "パスワードが一致しません",
),
"accountType": EzValidator<String>()
.required()
.oneOf(["personal", "business"]),
"companyName": EzValidator<String>().when(
"accountType",
(value, [ref]) {
if (ref?["accountType"] == "business" && (value == null || value.isEmpty)) {
return "法人アカウントの場合、会社名は必須です";
}
return null;
},
),
});
条件付き検証(dependsOn)
final EzSchema carRentalSchema = EzSchema.shape({
"age": EzValidator<int>().required().min(18),
"carType": EzValidator<String>()
.required()
.oneOf(["economy", "standard", "luxury"]),
"insuranceRequired": EzValidator<bool>().dependsOn(
condition: (ref) => ref!["age"] < 25 || ref["carType"] == "luxury",
then: EzValidator<bool>()
.required()
.addMethod((v) => v == true ? null : "保険への加入が必須です"),
orElse: EzValidator<bool>(optional: true),
),
});
データ変換(transform)
final EzSchema userProfileSchema = EzSchema.shape({
"name": EzValidator<String>()
.transform((value) => value.trim())
.minLength(3, "名前は3文字以上必要です"),
"email": EzValidator<String>()
.transform((value) => value.toLowerCase().trim())
.required()
.email(),
"phone": EzValidator<String>()
.transform((value) => value.replaceAll(RegExp(r'[^\d+]'), ''))
.matches(RegExp(r'^\+?\d{10,15}$'), "有効な電話番号を入力してください"),
});
ネストした検証
final EzSchema orderSchema = EzSchema.shape({
"orderId": EzValidator<String>().required().uuid(),
"customer": EzSchema.shape({
"name": EzValidator<String>().required(),
"email": EzValidator<String>().required().email(),
"phone": EzValidator<String>().required().phone(),
}),
"shippingAddress": EzSchema.shape({
"street": EzValidator<String>().required(),
"city": EzValidator<String>().required(),
"prefecture": EzValidator<String>().required(),
"postalCode": EzValidator<String>()
.required()
.matches(RegExp(r'^\d{3}-\d{4}$'), "郵便番号の形式が正しくありません"),
}),
"items": EzValidator<List<Map<String, dynamic>>>()
.required()
.minLength(1, "商品を1つ以上選択してください")
.arrayOf(
EzValidator<Map<String, dynamic>>().schema(
EzSchema.shape({
"productId": EzValidator<String>().required(),
"quantity": EzValidator<int>().required().min(1).max(100),
"price": EzValidator<double>().required().positive(),
}),
),
),
});
Union型の検証
final EzSchema paymentSchema = EzSchema.shape({
'paymentMethod': EzValidator<String>()
.required()
.oneOf(['card', 'bank', 'cash']),
'paymentDetails': EzValidator().union([
// クレジットカード用のスキーマ
EzValidator<Map<String, dynamic>>().schema(
EzSchema.shape({
'cardNumber': EzValidator<String>()
.required()
.matches(RegExp(r'^\d{16}$')),
'cvv': EzValidator<String>()
.required()
.matches(RegExp(r'^\d{3,4}$')),
}),
),
// 銀行振込用のスキーマ
EzValidator<Map<String, dynamic>>().schema(
EzSchema.shape({
'accountNumber': EzValidator<String>().required(),
'branchCode': EzValidator<String>().required(),
}),
),
// 現金払いの場合はnull
EzValidator<String?>().addMethod((v) => v == null ? null : '現金払いの場合、詳細は不要です'),
]),
});
カスタムロケールの実装
class JapaneseLocale implements EzLocale {
const JapaneseLocale();
@override
String required([String? label]) => '${label ?? 'この項目'}は必須です';
@override
String email(String v, [String? label]) =>
'${label ?? 'メールアドレス'}の形式が正しくありません';
@override
String minLength(String v, int n, [String? label]) =>
'${label ?? 'この項目'}は$n文字以上必要です';
@override
String maxLength(String v, int n, [String? label]) =>
'${label ?? 'この項目'}は$n文字以下にしてください';
@override
String min(String v, num n, [String? label]) =>
'${label ?? 'この値'}は$n以上である必要があります';
@override
String max(String v, num n, [String? label]) =>
'${label ?? 'この値'}は$n以下である必要があります';
@override
String positive(String v, [String? label]) =>
'${label ?? 'この値'}は正の数である必要があります';
@override
String negative(String v, [String? label]) =>
'${label ?? 'この値'}は負の数である必要があります';
@override
String date(String v, [String? label]) =>
'${label ?? 'この項目'}は有効な日付である必要があります';
@override
String url(String v, [String? label]) =>
'${label ?? 'URL'}の形式が正しくありません';
@override
String phone(String v, [String? label]) =>
'${label ?? '電話番号'}の形式が正しくありません';
@override
String ip(String v, [String? label]) =>
'${label ?? 'IPアドレス'}の形式が正しくありません';
@override
String ipv6(String v, [String? label]) =>
'${label ?? 'IPv6アドレス'}の形式が正しくありません';
@override
String uuid(String v, [String? label]) =>
'${label ?? 'UUID'}の形式が正しくありません';
@override
String boolean(String v, [String? label]) =>
'${label ?? 'この値'}はtrue/falseである必要があります';
@override
String lowerCase(String v, [String? label]) =>
'${label ?? 'この項目'}は小文字である必要があります';
@override
String upperCase(String v, [String? label]) =>
'${label ?? 'この項目'}は大文字である必要があります';
// その他のメソッドも同様に実装...
}
// 使用方法
void main() {
EzValidator.setLocale(const JapaneseLocale());
// これ以降、すべての検証エラーメッセージが日本語で表示されます
}
実践的な例
ユーザー登録フォーム
class UserRegistrationForm extends StatefulWidget {
@override
_UserRegistrationFormState createState() => _UserRegistrationFormState();
}
class _UserRegistrationFormState extends State<UserRegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _ageController = TextEditingController();
bool _agreeToTerms = false;
late final EzSchema registrationSchema;
@override
void initState() {
super.initState();
registrationSchema = EzSchema.shape({
"email": EzValidator<String>()
.required("メールアドレスは必須です")
.email("有効なメールアドレスを入力してください"),
"password": EzValidator<String>()
.required("パスワードは必須です")
.minLength(8, "パスワードは8文字以上必要です")
.matches(
RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)'),
"大文字、小文字、数字を含む必要があります",
),
"confirmPassword": EzValidator<String>().when(
"password",
(value, [ref]) {
if (value != ref?["password"]) {
return "パスワードが一致しません";
}
return null;
},
),
"age": EzValidator<int>()
.required("年齢は必須です")
.min(18, "18歳以上である必要があります")
.max(120, "有効な年齢を入力してください"),
"agreeToTerms": EzValidator<bool>()
.required()
.addMethod((v) => v == true ? null : "利用規約に同意してください"),
});
}
void _handleRegistration() {
final formData = {
'email': _emailController.text,
'password': _passwordController.text,
'confirmPassword': _confirmPasswordController.text,
'age': int.tryParse(_ageController.text),
'agreeToTerms': _agreeToTerms,
};
final (data, errors) = registrationSchema.validateSync(formData);
if (errors.isEmpty) {
// 登録処理
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登録成功!')),
);
} else {
// エラー表示
String errorMessage = errors.entries
.map((e) => '${e.key}: ${e.value}')
.join('\n');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'メールアドレス'),
validator: EzValidator<String>()
.required("メールアドレスは必須です")
.email("有効なメールアドレスを入力してください")
.build(),
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(labelText: 'パスワード'),
validator: EzValidator<String>()
.required("パスワードは必須です")
.minLength(8, "パスワードは8文字以上必要です")
.build(),
),
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
decoration: InputDecoration(labelText: 'パスワード(確認)'),
validator: (value) {
if (value != _passwordController.text) {
return "パスワードが一致しません";
}
return null;
},
),
TextFormField(
controller: _ageController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: '年齢'),
validator: EzValidator<String>()
.required("年齢は必須です")
.addMethod((v) {
final age = int.tryParse(v ?? '');
if (age == null) return "有効な数値を入力してください";
if (age < 18) return "18歳以上である必要があります";
if (age > 120) return "有効な年齢を入力してください";
return null;
})
.build(),
),
CheckboxListTile(
title: Text('利用規約に同意します'),
value: _agreeToTerms,
onChanged: (bool? value) {
setState(() {
_agreeToTerms = value ?? false;
});
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_handleRegistration();
}
},
child: Text('登録'),
),
],
),
);
}
}
パフォーマンスのヒント
- スキーマの再利用: スキーマは一度定義して再利用する
// 良い例
final userSchema = EzSchema.shape({...});
// 悪い例 - 毎回新しいスキーマを作成
Widget build(BuildContext context) {
final schema = EzSchema.shape({...}); // 避けるべき
}
- 適切な検証タイミング: 必要に応じて検証を実行
// フィールドごとの検証
TextFormField(
validator: emailValidator.build(),
autovalidateMode: AutovalidateMode.onUserInteraction,
)
// フォーム全体の検証
if (_formKey.currentState!.validate()) {
// 送信処理
}
- エラーメッセージのキャッシュ: 頻繁に使用するエラーメッセージは定数として定義
class ValidationMessages {
static const required = '必須項目です';
static const invalidEmail = '有効なメールアドレスを入力してください';
static const passwordTooShort = 'パスワードは8文字以上必要です';
}
まとめ
EzValidatorは、Flutterアプリケーションにおける入力検証を簡単かつ柔軟に行うための強力なツールです。シンプルなAPIから始めて、必要に応じて高度な機能を活用することで、堅牢な入力検証システムを構築できます。