Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Entity

constraints

[Required] · [Unique] · [MaxLength(n)] · [MinLength(n)] · [Min(n)] · [Max(n)] · [Pattern(regex)] · [Immutable] · [Precision(p, s)] · [MaxBytes(n)]

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the storage-shaping Immutable, Precision and MaxBytes. They are checked when the row is written, so a bad row cannot reach the database from any code path.

stable6 examples compiled by CIentitymodelvalidation

Summary#

Constraints are per-member rules enforced when the row is written. They are not form validation you can forget to call: a row that breaks one cannot reach the database, whatever code path tried to write it — a function, an import, a test, an API call.

Signature#

[Unique(<Member>, <Member>)]                      // …or over a COMBINATION, declared on the entity
entity <Name> {
  [Required]                 <Type>   <Member>;   // must be present
  [Unique]                   string   <Member>;   // no two rows may share a value
  [MaxLength(<n>)]           string   <Member>;   // bounded text
  [Min(<n>), Max(<n>)]       int      <Member>;   // an inclusive numeric range
  [Pattern("<regex>")]       string   <Member>;   // must match
}

Every constraint takes an optional message as its last argument — what the user is told when the rule refuses them:

  [Pattern("<regex>", "<message>")]  string  <Member>;
  [MaxLength(<n>, "<message>")]      string  <Member>;
  [Required("<message>")]            <Type>  <Member>;
  [Unique("<message>")]              string  <Member>;   // shown when the value collides with another row

…including the entity-level composite form, where the message goes last, after the members:

[Unique(<Member>, <Member>, "<message>")]
entity <Name> { … }

[Unique] is the one whose default wording helps least — a collision otherwise surfaces as the raw index name — so its message is worth writing: [Unique("That slug is already taken.")].

Description#

Which constraint attributes are there?#

AttributeRuleOn a null value
[Required]the member must be present when the row is writtenthis is the one that rejects null
[Unique]no two rows may hold the same valuepasses — nulls do not collide
[MaxLength(n)] · [MinLength(n)]text is at most / at least n characterspasses
[Min(n)] · [Max(n)]an inclusive numeric rangepasses
[Pattern("…")]text matches the regular expressionpasses
[Immutable]may be set on create, but never updatedpasses (it governs updates, not presence)
[Precision(p, s)]a decimal stored with p total digits and s after the pointpasses
[MaxBytes(n)]the value's stored size is at most n bytespasses

Read that last column carefully: every constraint except [Required] lets null through. That is the correct behaviour — "if there is a value, it must look like this" is a different rule from "there must be a value" — but it surprises people. If a code must be present and well-formed, say both:

entity Product {
  [Required, Pattern("^[A-Z]{3}$")] string Code;   // must exist, and must be three capitals
  [Pattern("^[A-Z]{3}$")] string AltCode;          // may be absent; if present, must match
}

Give a [Pattern] a message. Every other constraint refuses in words a person can act on — "Email is required", "Name is too long". A pattern refuses with the regular expression, which explains nothing to the person reading it:

entity Product {
  [Required, Pattern("^[A-Z]{3}$", "must be three capital letters")] string Code;
}

Can I stack several on one member?#

Attributes stack, in one bracket or several — whichever reads better:

entity Coupon {
  [Required, Unique, MaxLength(20)] string Code;   // present, one of a kind, bounded
  [Min(1), Max(100)] int PercentOff;               // inclusive: 1 and 100 both pass
  [MaxLength(500)] string Notes;                   // optional, but bounded when given
}

[Required] on a reference forbids an orphan#

[Required] works on a reference too, and it is how you say "this child cannot exist without its parent":

entity Order {
  [Required] string Code;
}

entity LineItem {
  [Required] Order Order;   // a line with no order is a violation, not a stray row
  decimal Amount;
}

[Unique] is enforced by the database#

[Unique] is a real unique index, not a check-then-insert in application code. That distinction matters under concurrency: two requests racing to claim the same coupon code cannot both win, because the second one is rejected by the database rather than by a check that already passed.

AND A SWAP BETWEEN TWO EXISTING ROWS IS REFUSED — the whole commit rolls back, silently. This is the one interaction an ordered list actually performs, and it is the opposite of what most people assume, so it is worth stating plainly:

[Unique] int Position;
// …then, in an action:
int mine = job.Position;
job.Position  = above.Position;   // ← both rows now hold the same value for an instant
above.Position = mine;
UnitOfWork.Commit();              // ← the index refuses; NOTHING is written

A unique INDEX is checked per statement, not at commit, so the intermediate state where two rows share a value is rejected even though the state you were committing is legal. Measured: the two rows come back unchanged, and before 2026-08-29 the refusal reached nobody — no error, no banner, no fault in the test. It now surfaces in a failing test as action 'MoveUp' failed: Constraint violation: 'Job.Position' must be unique, but the write still does not land.

So for a position column, do one of these:

leave it un-[Unique]the ordinary answer. A rank has no meaning in a gap or a repeat beyond "which comes first", and nothing else depends on it being distinct.
write the midpoint instead of swappingjob.Position = (above.Position + below.Position) / 2m over a decimal — one row changes, so no two rows ever collide. This also stops a move rewriting the whole tail.

[Unique] is not deferrable today, and that is why. Foreign keys are (they are emitted as deferrable constraints); [Unique] is emitted as a CREATE UNIQUE INDEX, and Postgres cannot defer an index. Making it deferrable would make the swap above work as written — it is a real option and it is not built.

Over a COMBINATION of members, write it on the entity[Unique(A, B)] above the declaration, naming two or more members. It is the same real unique index, over the pair — and it takes the same optional message, written last: [Unique(A, B, "…")]. That is one form, not two, and it is the one to reach for. Without the message the person is shown the index's own words; with it, they are told what they did:

entity Channel {
  [Required, MaxLength(60)] string Name;
}

[Unique(Channel, Person, "They are already in this channel.")]
entity Membership {
  [Required] Channel Channel;
  [Required] User Person;
}

[Principal] entity User {
  [Required, MaxLength(200)] string Email;
}

This is the constraint a join table wants, and reaching for a Membership.Any(…) guard instead is the mistake the paragraph above describes: two requests can both read "not a member" before either writes, and only the index is enforced where that race is. Keep the guard as well if you want a quiet no-op on a double-click — but it is the convenience, not the rule.

Which field does the violation land on? For a composite one, a field whose name is the members joined with ", ", in declaration orderChannel, Person for the Membership above. Not either member on its own: the rule is about the combination, so what is refused is the combination, named as one thing. That is what a test aims at, and it is the one string you need:

Assert.Violation("Channel, Person");                                // the pair collided
Assert.Violation("Channel, Person", "already in this channel");     // …and this is what it says

A single-member [Unique] is the ordinary case — the field is just the member. Either way the violation is raised by the server, at the save, because "is this taken?" is a question about other rows; see [[testing-ui#composite-unique]] for when a test may assert it.

⚠ A unique constraint is over a table, and one table holds a whole inheritance hierarchy — so one declared on a base spans every type derived from it. That is usually what you want, and the compiler says so either way (it warns, naming every type covered).

Write-once, decimal precision, and size bounds#

Four constraints shape a value beyond "is it valid":

  • [Immutable] is a rule about updates, not presence: the member may be set when the row is created and then never changed. It is how you say "an order's placed-date is written once" — an attempt to update it is refused, from any code path. Pair it with [Required] when the value must also be present from the start.
  • [MinLength] is the floor to [MaxLength]'s ceiling: [MinLength(n)] requires text of at least n characters.
  • [Precision] pins how a decimal is stored: [Precision(p, s)] gives p total significant digits, s of them after the decimal point. [Precision(10, 2)] is the shape of money — up to eight digits before the point, two after.
  • [MaxBytes] bounds the stored size of a value in bytes: [MaxBytes(n)] caps a large field — a Json document or rich text — where the limit you care about is storage, not character count.
entity Invoice {
  [Required, Immutable] DateTime IssuedAt;      // set once, at creation; never edited afterwards
  [Precision(10, 2)] decimal Amount;            // money: 8 digits before the point, 2 after
  [MinLength(3), MaxLength(20)] string Number;  // a bounded reference code
  [MaxBytes(1048576)] Json Payload;             // at most 1 MB of stored JSON
}

The rule spans several members — what then?#

A constraint speaks about one member. When the rule spans several — "shelf days must be under 30, but only for perishable items" — you want an invariant.

See also#

Related

entity members

The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type…

invariant

A row-level rule spanning several members, checked when the row is written. Use it when a constraint on one member is…

relations

One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back…

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…