Skip to content

Validation rules

This page lists every annotation you can put on a @Dto field.

See Validation for how DTOs work.

How to read the tables:

  • Code is the error code. Use it in @Dto(messages: ...) and in a MessageResolver.
  • Message is the default English text. {field} is the last key of the path.
  • Every annotation also takes message:. It wins over every other message.
  • All limits include the limit itself, unless the table says "exclusive".
  • On a List<T> field, a value rule checks each item. Only the list rules check the list itself.

Type errors

These have no annotation. They come from the field type.

Code When Message
required a required field is missing, null, or "" The {field} is required
string not a string The {field} must be a string
integer not a whole number The {field} must be an integer
numeric not a number (double, num) The {field} must be a number
boolean not a bool The {field} must be a boolean
date not an ISO 8601 date The {field} must be a date
in not a name of the enum The selected {field} is invalid. Valid options are {values}
array not a list The {field} must be an array
object not an object (nested DTO, Map) The {field} must be an object
file not a file The {field} must be a file
unrecognized_keys unknown key, with @Dto(strict: true) The {field} field is not allowed

Text

For String fields (and List<String> items).

Annotation Code Message JSON Schema
@Length(min: 3, max: 50) length_between The {field} must be between {min} and {max} characters minLength, maxLength
@Length(min: 6, max: 6) length The {field} must be exactly {length} characters minLength, maxLength
@MinLength(3) min_length The {field} must be at least {min} characters minLength
@MaxLength(50) max_length The {field} may not be greater than {max} characters maxLength
@Email() email The {field} is not a valid email format: email
@Url() url The {field} must be a url format: uri
@Uuid() uuid The {field} is invalid uuid format: uuid
@Ip() ip The {field} must be an ip address anyOf ipv4 / ipv6
@Regex(r'[a-z]+') regex The {field} format is invalid pattern
@In(['a', 'b']) in The selected {field} is invalid. Valid options are {values} enum
@NotIn(['root']) not_in The {field} field cannot be {value} not: {enum}
@StartsWith('dox') start_with The {field} must start with {prefix} pattern
@EndsWith('.dev') end_with The {field} must end with {suffix} pattern
@Alpha() alpha The {field} must be an alphabetic pattern
@AlphaNumeric() alpha_numeric The {field} must be only alphabetic and number pattern
@AlphaDash() alpha_dash The {field} must be only alphabetic, number, dash and underscore pattern

Details:

  • @Length has min: and max:. Both are optional, but you must give one. With only min: it acts like @MinLength, with only max: like @MaxLength.
  • Length counts characters (Unicode code points). é counts as 1.
  • @Url() needs http or https and a host.
  • @Uuid() takes any version. Upper or lower case.
  • @Regex(pattern, {message}) must match the whole value. You do not need ^ and $. The regex is checked when the generator runs. A bad regex stops the build.
  • @Alpha(), @AlphaNumeric() and @AlphaDash() use ASCII letters a-z, A-Z. @AlphaDash() also allows digits 0-9, - and _.
  • @In and @NotIn compare exactly (case matters).

More text rules

For String fields. Like the Laravel rules with the same names.

Annotation Code Message JSON Schema
@Json() json The {field} is not a valid json contentMediaType: application/json
@Digits(6) digits The {field} must be {digits} digits pattern
@DigitsBetween(4, 6) digits_between The {field} must be between {min} and {max} digits pattern
@DateFormatted('Y-m-d') date_format The {field} must match the format {format}
@HexColor() hex_color The {field} must be a hex color like #1a2b3c pattern
@Password(min: 8, ...) password The {field} must have {rules} minLength
  • @Digits and @DigitsBetween take only 0-9. No sign, no dot. Use a String field, so leading zeros stay.
  • @DateFormatted uses the PHP date() letters: day d, j, D (Mon), l (Monday), N, w, S (st), z; month m, n, M (Jan), F (January), t; year Y, y, L; time a, A, g, h, G, H, i, s, v, u; time zone e, T, P, p, O, Z, I; full U, c, r. Put \ before a letter to use it as text, like Y-m-d\TH:i. Any other letter stops the build (and throws an ArgumentError in v.string().dateFormat()). Other characters must be there as they are. Like Laravel, j, n, g, G, z and U take no leading zero: j/n/Y rejects 01/02/2024. e and T take any name that looks like a time zone. They do not check that it is a real one. The date must exist: 2023-02-29 fails. Names need the case shown (Jan, not jan). A day name must fit the date. The field stays a String. Use a DateTime field for ISO 8601 dates.
  • @Password options: min (default 8), letters, mixedCase (one upper and one lower case letter), numbers, symbols. They are all off by default. Letters, numbers and symbols are Unicode, so é is a letter. {rules} lists the options you set, like at least 8 characters, one number, and one symbol, or at least 8 characters and one number. It does not check leaked passwords (Laravel uncompromised).

Other fields

These read another key of the same object. The other key is the input key, like account_type, not the Dart name accountType. If you give the Dart name of a field with another key, the build stops. @Confirmed and @Same compare the raw input of both keys, like Laravel. So @Trim does not change the compare.

Annotation Code Message
@Confirmed() confirmed The {field} confirmation does not match
@Same('email') same The {field} must match {other}
@RequiredIf('type', 'company') required_if The {field} is required when {other} is {values}
@RequiredWith(['phone']) required_with The {field} is required when {others} is present
  • @Confirmed() on password needs the key password_confirmation with the same value. The error is on password. Use @Confirmed(other: 'password_again') for another key. The DTO does not need a field for the confirmation key.
  • Text and numbers match by text, both ways: 1234 matches "1234", and "1234" matches 1234.
  • @RequiredIf and @RequiredWith work on a nullable field, a Maybe field or a field with a default. On an always-required field the build stops.
  • @RequiredIf('type', ['company', 'shop']) takes a list: any of the values makes the field required. "true" matches true, "1" matches 1. An enum value matches its name: @RequiredIf('role', Role.admin) matches "admin".
  • @RequiredWith(['phone', 'email']): the field is required when any of the keys is there and not empty.
  • Missing means: not there, null, blank text, an empty list or an empty map.
  • They run in step 1, with the other "required" checks. @Confirmed and @Same run in step 2.

Change text

These change the value before the rules run. They always run in this order, wherever you put them: @Trim, then @Lowercase or @Uppercase, then the "empty string is missing" check.

Annotation Does
@Trim() removes spaces at the start and the end
@Lowercase() makes the text lower case
@Uppercase() makes the text upper case
@AllowEmpty() keeps "" as a value. Without it, "" counts as missing.

They have no error and no JSON Schema output. @Lowercase() and @Uppercase() together stop the build.

Numbers

For int, double and num fields (and list items).

Annotation Code Message JSON Schema
@Min(18) min The {field} must be greater than or equal {min} minimum
@Max(100) max The {field} must be less than or equal {max} maximum
@Range(1, 10) between The {field} must be between {min} and {max} minimum, maximum
@Positive() greater_than The {field} must be greater than 0 exclusiveMinimum: 0
@GreaterThan(0.5) greater_than The {field} must be greater than {min} (exclusive) exclusiveMinimum
@LessThan(100) less_than The {field} must be less than {max} (exclusive) exclusiveMaximum
@In([1, 2, 3]) in The selected {field} is invalid. Valid options are {values} enum
@NotIn([0]) not_in The {field} field cannot be {value} not: {enum}

The values are num. @Min(1.5) on an int field stops the build. @Range is Laravel between.

Lists

For List<T> fields. These check the list, not the items.

Annotation Code Message JSON Schema
@MinItems(1) min_items The {field} must have at least {min} items minItems
@MaxItems(5) max_items The {field} may not have more than {max} items maxItems
@UniqueItems() unique_items The {field} must not have duplicate items uniqueItems: true

@UniqueItems() works for String, numbers, bool, enums and DateTime. On a list of DTOs it stops the build.

Dates

For DateTime fields.

Annotation Code Message JSON Schema
@After('2000-01-01') after The {field} must be a date after {date} format: date-time
@Before('now') before The {field} must be a date before {date} format: date-time
  • The value is an ISO 8601 date, or 'now' (the time of the request). A bad date text stops the build.
  • Both are exclusive: @After('2000-01-01') does not accept 2000-01-01T00:00:00Z.
  • To compare two fields (end after start), use validate().

Files

For RequestFile and List<RequestFile> fields.

Annotation Code Message JSON Schema
@MaxSize('2MB') max_size The {field} may not be larger than {max} maxLength (bytes)
@Mimes(['pdf', 'png']) mimes The {field} must be a file of type: {types} contentMediaType
@Image() image The {field} must be an image contentMediaType: image/*
@MinSize('10KB') min_size The {field} must be at least {min} minLength (bytes)
@MimeTypes(['image/png', 'image/*']) mime_types The {field} must be a file of type: {types} contentMediaType
@Dimensions(minWidth: 100, ratio: 3 / 2) dimensions The {field} has invalid image dimensions contentMediaType: image/*
  • @MaxSize and @MinSize units: B, KB, MB, GB. 1KB = 1024 bytes. A bad size text stops the build.
  • @Mimes takes file extensions. Dox finds the real type from the first bytes of the file. The file name extension must not name another type, or a type a browser runs (like html or svg). So x.html with GIF bytes does not pass. An empty or unknown extension is fine. For zip based types (like docx or xlsx), the file name extension must match too. For types without a byte signature (like txt or csv), it uses the file name extension.
  • @Image() is @Mimes(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']). No svg.
  • @MimeTypes takes MIME types. Dox turns each one into file extensions, then checks the real bytes like @Mimes. It does not trust the content type the client sent. image/* means: jpg, jpeg, png, gif, webp, bmp, tif, tiff, ico, heic, heif, avif and psd. No svg. This is wider than @Image(), and browsers can not show most of the extra types. For images that browsers can show, use @Image() or list the MIME types. Write image/jpeg, not image/jpg. image/jpg is not a real MIME type, so it never matches. An unknown MIME type never matches.
  • @Dimensions checks the width and height in pixels. Limits: width, height (exact), minWidth, maxWidth, minHeight, maxHeight and ratio (width / height, like 3 / 2). Give at least one limit, or the build stops. It reads only the image header of PNG, GIF, WebP and JPEG files, never the whole file. Any other file, or a broken or cut short header, fails. For a JPEG over 1 MB, Dox reads the first 128 KB. A JPEG with more photo metadata than that before its size fails. Like Laravel, a ratio passes when it is off by less than one pixel.
  • The JSON Schema of a file is {"type": "string", "format": "binary"}, plus the keys above.

With fluent schemas, the same rules are on v.file(). Sizes there are in bytes, not text:

v.file()
    .maxSize(2 * 1024 * 1024) // 2 MB
    .mimeTypes(<String>['image/jpeg', 'image/png'])
    .dimensions(minWidth: 100, maxWidth: 2000);

The fluent methods are maxSize(bytes), minSize(bytes), mimes(types), mimeTypes(types), dimensions(...), image() and extensions(types).

Database

From dox_orm. Async: they run after all other checks passed.

Annotation Code Message
@Unique(table, {column, ignoreParam, ignoreColumn, connection}) unique The {field} has already been taken
@Exists(table, {column, connection}) exists The selected {field} is invalid
  • @Unique column default: the input key. @Exists column default: id.
  • They read the table, not the model. So soft-deleted rows count too.
  • ignoreParam: 'id' skips the row where ignoreColumn (default id) equals the path param {id}.
  • They add nothing to the JSON Schema.

See Database rules.

In a fluent schema, use v.string().unique('users', column: 'email') and v.int().exists('teams'). See Fluent schemas.

Keys and DTO options

Annotation Does
@Name('emailAddress') the input key for this field. Default: the field name in snake_case.
@Dto() marks the class.
@Dto(source: Source.query) where to read: Source.auto (default), Source.query, Source.body.
@Dto(keyCase: KeyCase.field) keys: KeyCase.snake (default) or KeyCase.field (the Dart name as is).
@Dto(strict: true) unknown keys are errors. Default false: they are dropped.
@Dto(emptyStringsToNull: true) "" is read as null, not as missing. Default false.
@OrDefault() on a field: a bad value gives the default (or null), not an error. Errors from @Unique, @Exists and validate() are not dropped.
@Dto(messages: {...}) messages by code or key.code. A dotted path like address.city.required or tags.*.required works too.
@Describe('...') on a field: a short text for the description of the field in the JSON Schema. It does not change the checks.

A /// doc comment on a field becomes its description in the JSON Schema. @Describe wins over the /// comment. Use it when the text is for API users and MCP clients, not for the code. The OpenAPI docs and MCP tools (McpDtoTool) show it.

@Describe('Category ID (see list_categories).') final int categoryId;

Types in detail

How each field type reads input. The short table is in Types.

  • JSON is strict. In a JSON body, "18" is not an int. You get the error integer.
  • Query and form data are text. Dox turns the text into the field type first. If it can not, you get the type error.
  • int takes 18 or 18.0, never a fraction. "4.5" or 4.5 gives integer.
  • bool in text takes true, 1, yes, on and false, 0, no, off (any case).
  • DateTime takes 2024-01-31 or 2024-01-31T10:00:00Z. A value without a time zone is read as UTC. The result is always UTC (isUtc == true). A number (timestamp) is not allowed.
  • An enum matches the Dart name exactly. "Admin" does not match Role.admin. Custom wire values (like "ADMIN") are not supported.
  • Object? and dynamic take any value. They are not checked.
  • Map<String, dynamic> must be an object. Its values are not checked.
  • In query and form data, one value for a List<T> field gives a list with one item.
  • In bracket keys, list indexes are sorted and packed: items[3] alone gives a list with one item.
  • In query and form data, a list can have at most 1000 items, and keys can nest at most 10 levels. More gives 422 with the key _root.

Body types

Body Result
application/json (or any +json type) read with strict JSON types
application/x-www-form-urlencoded, multipart/form-data read as text, then converted
any other content type 415 Unsupported Media Type
JSON that is not valid 400 Bad Request
JSON that is not an object (like [1, 2]) 422 with the key _root

The order of checks

  1. Read the input and convert types. Change text (@Trim, @Lowercase, @Uppercase). Find missing required fields.
  2. The field rules, top to bottom.
  3. The validate() method (nested DTOs first, then the parent).
  4. Async rules (@Unique, @Exists, your AsyncRule), one at a time, in field order.

Each step runs only when the step before it had no errors. So the database is never asked about input that is already wrong.

  • Steps 1 and 2 run per DTO. So one answer can have a type error in address and a rule error in items.1.
  • Inside one step, Dox collects the errors of all fields.
  • A default value is not checked by the field rules. Async rules check the final value.

What the generator checks

The build stops with a clear error when:

  • a field is not final, or is not a parameter of the unnamed constructor,
  • a field type is not supported (see Types),
  • a rule does not fit the field type (like @Email() on an int),
  • a Maybe<T> field has no const Maybe.missing() default,
  • @RequiredIf or @RequiredWith is on a field that is always required,
  • @RequiredIf, @RequiredWith, @Same or @Confirmed(other:) names a Dart field (like accountType) instead of its input key (account_type),
  • @DateFormatted has a letter it does not know,
  • two @Dto classes in the app have the same name,
  • the class is generic, or extends another class (other than Object).