Usage

Key Concept - Mandatoriness

One of the most important concepts to remember is that all constraints on a property (other than @NotNull) are only applied to a property when its value is not null.

So for example, you may have a constraint that defines what makes an email address valid. You can apply this to optional properties as well as mandatory ones. So for example if you have a mandatory home email and an optional work email you may have something like

So in the case of Contacts the homeEmail property must have a value and it must satisfy isValidEmail. However, workEmail is valid if it is null, but if it does have a value it must also satisfy isValidEmail.

Define Constraints On Your Objects

Note: It is recommended that you use one of the core constraints when one exists for your need. This will allow you to take advantage of packages that may be built in the future such as code generators from JSON schema that support them. See the section on Core Constraints below

The following (rather contrived) example illustrates several features of constraints, which are described below.

isPositive is a const matcher that comes with the matcher library. In short a primate's age will satisfy this constraint if it is greater than 0.

Using matchers is a common way to specify constraints. When matcher based constraints are violated they provide details about what went wrong.

Constraint Inheritance

If you look at the Person class you will see that it extends Primate. This means that it will automatically inherit the age property and the isPositive constraint on it. That is, Persons will also be subject to this constraint.

You can also see that Person has a getter on age as follows

@Ensure(isBetween10and90)
int get age => super.age;

It simply redirects to the primate's age and exists soley so that we can further constrain age (admittedly with a rather silly constraint). isBetween10and90 is another matcher but this time not a const so we must use a function to wrap it as follows

In addition to matchers, you can also use plain ol' Dart code for your constraints. Note: as Dart does not have null safe path expressions you need to check each segment or risk an NPE

Note that even though this constraint depends only on a single field of the Address class it is not defined on the Address class's street property. The reason is, that it is not intended to be true for all uses of Address, just those that are owned by Persons. Keep this in mind when you decide where constraints should live.

Note the second argument person. This is the Person object that owns the parents field being validated. As you can see, this was needed to express this constraint. Most constraints don't need it but it's very useful at times.

Class Based Constraints

If we jump back to the Person class you will notice a constraint on the class itself

@Ensure(eitherOlderThan20OrHasAtLeastTwoAddresses,
description: 'Must be either older than twenty or have at least two adresses')

This is where you put cross field constraints. In other words, constraints that require more than one field of the class to express.

There is nothing terribly interesting about the constraint itself. What's interesting is in the context of validating a Person.

In order for the addresses property of Person to be considered valid it requires that each Address object is also valid. This means that the street property of each address must be at least 10 characters in length.

Constraint violated at path Symbol("addresses").Symbol("street")
Expected: an object with length of a value greater than or equal to <10>
Actual: '15 x st'
Which: has length of <9>
Constraint violated at path Symbol("parents")
A person cannot be their own parent
Constraint violated at path Symbol("age")
Expected: (a value greater than or equal to <10> and a value less than or equal to <90>)
Actual: <-22>
Which: is not a value greater than or equal to <10>
Constraint violated at path Symbol("age")
Expected: a positive value
Actual: <-22>
Which: is not a positive value
Constraint violated at path
Must be either older than twenty or have at least two adresses
Expected: (Person with age that a value greater than <20> or Person with addresses that an object with length of a value greater than or equal to 'two')
Actual: Person:<Person[age: -22, addressses: [Address[street: 15 x st]]]>

Note, depending on the audience you may not simply print the violations like this. Just like in Java the ConstraintViolation class is a structured object so in addition to a message you can get lots of details about exactly what was violated where.

When integrating with UI frameworks like polymer, you would typically use the structured information to provide better error messages. Specifying Constraintdescriptions provide you complete control the wording of a constraint and is typically what you would want to show to the user.

Validating Groups

The model contained a single group called Detailed that was applied to the addresses property.

It was excluded from validation in the previous example which was validating against the DefaultGroup

Pattern allows you to constrain a String field with anything that implements the Pattern class. By default it assumes you give it a RegExp and does the conversion (because RegExp does not have a const constructor)

You can prevent the conversion to RegExp with th isRegExp parameter

@Pattern('a plain ol string', isRegExp: false)
String id;

Note: dart:core defines a class called Pattern. Using constrains Pattern will result in a warning that it is hiding the dart:core version. To get rid of this warning you need to add import 'dart:core' hide Pattern; or else import constrain with a prefix like import 'package:constrain/constrain.dart' as c;.

To avoid this name clash Pattern will likely be renamed in the future

JSON Encoding

The rich model for constraint violations can be converted to JSON, for example to send it between a server and client.

The detailed information allows clients to be intelligent about how they report the errors to the user

This is the more common way to use matchers as most matchers take an argument and are accessed via function. These cannot currently be const in Dart so the workaround is to use a function that returns the matcher.

Constraint Groups

ConstraintGroups are used to restrict which constraints are validated during a validation.
The ConstraintGroup is defined as

Looking up Constraints

If you want to do something fancier, for example, integrate with some library (like Polymer, Json Schema etc.) then you may need to directly work with the constraints.

final resolver = new TypeDescriptorResolver();
final typeDescriptor = resolver.resolveFor(Person);
// now you can tranverse the descriptor to get all the constraints on this class and as transitively reachable.