Daravelv1.0.0
ENID GitHub

Validation

Validate a request inline with Laravel's rule strings, or with typed rule objects from Rules:

final data = await request.validate({
  'email': 'required|email|unique:users',
  'password': 'required|min:8|confirmed',
  'role': [Rules.required(), Rules.inList(['admin', 'editor'])],
  'tags.*': 'string|max:20',
});
data.string('email'); // typed access to validated input only

Failures throw ValidationException, rendered as Laravel's 422 body:

{
  "message": "The email field is required. (and 1 more error)",
  "errors": {
    "email": ["The email field is required."],
    "password": ["The password field must be at least 8 characters."]
  }
}

The default English messages are identical to Laravel 12's lang/en/validation.php (checked by the conformance suite against the upstream file). Indonesian ships as the second locale: set request.attributes['locale'] = 'id' (for example in a middleware).

Semantics locked to Laravel#

  • nullable stops the remaining rules when the value is null.
  • A field that is absent and not required skips non-implicit rules; sometimes validates only when the key is present.
  • confirmed looks for {field}_confirmation.
  • bail stops at the first failure of a field.
  • Nested data uses dot paths and * wildcards.

Form requests#

final class StorePostRequest extends FormRequest {
  const StorePostRequest();

  @override
  Map<String, Object> rules(Request request) => {
    'title': 'required|string|max:120',
    'body': 'nullable|string',
    'published': [Rules.sometimes(), Rules.boolean()],
  };
}

await FormRequest.run(const StorePostRequest(), request) runs authorize() (403 when it returns false), then the rules, and returns the validated data. dart run bin/dartisan.dart make:request StorePostRequest creates one.

Declare it on the route with .validates(const StorePostRequest()) instead and read request.validated() in the handler; see Routing.

File uploads#

multipart/form-data bodies are parsed as they arrive: fields become input like a form, and each file is streamed to a temporary file (deleted after the response) and appears as an UploadedFile. Names such as photos[] and user[avatar] nest as in Laravel.

router.post('/avatar', (request) async {
  final data = request.validated();
  final path = await data.file('avatar').store('avatars', disk: 'public');
  return {'url': Storage.disk('public').url(path)};
}).rules(const {'avatar': 'required|image|max:2048'});
  • request.file('avatar'), request.hasFile('photos') and request.uploadedFiles() read the uploads; all() includes them.
  • UploadedFile: clientName, clientExtension, clientMimeType (sent by the client, not trusted), mimeType and extension (detected from the content), size, bytes(), text(), openRead(), hashName(), store(directory, disk:) and storeAs(directory, name, disk:).
  • Rules, as in Laravel 12: file, image (jpg, jpeg, png, gif, bmp, webp; image:allow_svg adds SVG), mimes:pdf,docx (the extension of the detected type), mimetypes:image/*, extensions:csv,txt (the client's file name), and min, max, size, between in kilobytes. PHP files are refused unless php is listed.
  • Types are detected from the content (PDF, images, Office and OpenDocument files, archives, text), never from the file name: a text file named photo.png is not an image.
  • The body size limit of BodyLimitsMiddleware (50 MiB by default) covers uploads; a request also accepts at most 1 000 fields and 20 files.

In tests, send files with client.post(uri, files: {...}) (or Request.fake(files: ...)) and build them with UploadedFile.fake:

await client.post('/api/avatar', files: {
  'avatar': UploadedFile.fake.image('me.png', kilobytes: 120),
  'photos': [UploadedFile.fake.image('1.png'), UploadedFile.fake.image('2.gif')],
});