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 aMessageResolver. - 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:
@Lengthhasmin:andmax:. Both are optional, but you must give one. With onlymin:it acts like@MinLength, with onlymax:like@MaxLength.- Length counts characters (Unicode code points).
écounts as 1. @Url()needshttporhttpsand 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 lettersa-z,A-Z.@AlphaDash()also allows digits0-9,-and_.@Inand@NotIncompare 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 |
@Digitsand@DigitsBetweentake only0-9. No sign, no dot. Use aStringfield, so leading zeros stay.@DateFormatteduses the PHPdate()letters: dayd,j,D(Mon),l(Monday),N,w,S(st),z; monthm,n,M(Jan),F(January),t; yearY,y,L; timea,A,g,h,G,H,i,s,v,u; time zonee,T,P,p,O,Z,I; fullU,c,r. Put\before a letter to use it as text, likeY-m-d\TH:i. Any other letter stops the build (and throws anArgumentErrorinv.string().dateFormat()). Other characters must be there as they are. Like Laravel,j,n,g,G,zandUtake no leading zero:j/n/Yrejects01/02/2024.eandTtake any name that looks like a time zone. They do not check that it is a real one. The date must exist:2023-02-29fails. Names need the case shown (Jan, notjan). A day name must fit the date. The field stays aString. Use aDateTimefield for ISO 8601 dates.@Passwordoptions: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, likeat least 8 characters, one number, and one symbol, orat least 8 characters and one number. It does not check leaked passwords (Laraveluncompromised).
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()onpasswordneeds the keypassword_confirmationwith the same value. The error is onpassword. 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:
1234matches"1234", and"1234"matches1234. @RequiredIfand@RequiredWithwork on a nullable field, aMaybefield 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"matchestrue,"1"matches1. 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.
@Confirmedand@Samerun 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 accept2000-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/* |
@MaxSizeand@MinSizeunits:B,KB,MB,GB.1KB= 1024 bytes. A bad size text stops the build.@Mimestakes 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 (likehtmlorsvg). Sox.htmlwith GIF bytes does not pass. An empty or unknown extension is fine. For zip based types (likedocxorxlsx), the file name extension must match too. For types without a byte signature (liketxtorcsv), it uses the file name extension.@Image()is@Mimes(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']). Nosvg.@MimeTypestakes 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. Nosvg. 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. Writeimage/jpeg, notimage/jpg.image/jpgis not a real MIME type, so it never matches. An unknown MIME type never matches.@Dimensionschecks the width and height in pixels. Limits:width,height(exact),minWidth,maxWidth,minHeight,maxHeightandratio(width / height, like3 / 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, aratiopasses 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 |
@Uniquecolumn default: the input key.@Existscolumn default:id.- They read the table, not the model. So soft-deleted rows count too.
ignoreParam: 'id'skips the row whereignoreColumn(defaultid) 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.
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 anint. You get the errorinteger. - Query and form data are text. Dox turns the text into the field type first. If it can not, you get the type error.
inttakes18or18.0, never a fraction."4.5"or4.5givesinteger.boolin text takestrue,1,yes,onandfalse,0,no,off(any case).DateTimetakes2024-01-31or2024-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
enummatches the Dart name exactly."Admin"does not matchRole.admin. Custom wire values (like"ADMIN") are not supported. Object?anddynamictake 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
422with 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
- Read the input and convert types. Change text (
@Trim,@Lowercase,@Uppercase). Find missing required fields. - The field rules, top to bottom.
- The
validate()method (nested DTOs first, then the parent). - Async rules (
@Unique,@Exists, yourAsyncRule), 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
addressand a rule error initems.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 anint), - a
Maybe<T>field has noconst Maybe.missing()default, @RequiredIfor@RequiredWithis on a field that is always required,@RequiredIf,@RequiredWith,@Sameor@Confirmed(other:)names a Dart field (likeaccountType) instead of its input key (account_type),@DateFormattedhas a letter it does not know,- two
@Dtoclasses in the app have the same name, - the class is generic, or extends another class (other than
Object).