Property
A Property item inside a Schema declares a single field on a governed type — its name, type, cardinality, default, uniqueness, references, cascade behaviour, group membership, and resolvers.
When to reach for it
Declare a Property for every itemprop on a governed type that should be checked, defaulted, transformed, or constrained. An itemprop with no matching Property declaration is ignored by validation — only declared properties are checked.
Shape
A property is a nested [itemscope] inside a schema, linked via itemprop="property".
<div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="email">
<meta itemprop="type" content="https://schema.host/Text">
<meta itemprop="cardinality" content="1..1">
<meta itemprop="default" content="user@example.com">
<meta itemprop="unique" content="true">
<meta itemprop="references" content="https://example.com/Org#identifier">
<meta itemprop="cascade" content="restrict">
<meta itemprop="group" content="contact-info">
<script type="text/sessel" itemprop="@validate">
self.all((el) => el.text().matches("^[^@]+@[^@]+\\.[^@]+$"))
</script>
</div>
Fields
| Field | Cardinality | Accepts | Purpose |
|---|---|---|---|
name |
1..1 |
Text | Matches the itemprop on data items. A property with no name is silently skipped. |
type |
0..1 |
URL | A primitive type URL from schema.host or another schema's governed type URL. When omitted, no type validation runs. |
cardinality |
0..1 |
Text | 0..1, 1..1, 0..n, 1..n. Defaults to 0..n. |
description |
0..1 |
Text | Human-readable description for tooling and error messages. |
default |
0..1 |
Text or Sessel item | Value injected when the property is absent on a write. See below. |
unique |
0..n |
Text | "true" for individual uniqueness; any other string is a composite-uniqueness group name. |
references |
0..1 |
Text | Foreign key in the form {itemtype}#{itemprop}. The target must be declared unique: true. |
cascade |
0..1 |
Text | "true" (cascade delete) or "restrict" (block delete). Requires references. |
group |
0..n |
Text | Group name(s) for required combinations. |
@validate |
0..1 |
Sessel or JavaScript binding | Must return truthy. self/first arg is the property value. Fires at persistence time. See Resolvers. |
@write |
0..1 |
Sessel or JavaScript binding | Transforms property values before storage. See Resolvers. |
@read |
0..1 |
Sessel or JavaScript binding | Transforms property values on read. See Resolvers. |
@key |
0..1 |
Boolean ("true" / "false") |
Marks this property as the schema's primary key. The instance's value for this property is emitted as id="…" on the root element when the instance is materialised via Pagelove.PUT. Requires unique: "true" on the same property. See below. |
Cardinality values
| Value | Meaning |
|---|---|
0..n |
Any number of values allowed. Default when cardinality is omitted. |
0..1 |
At most one value. |
1..1 |
Exactly one value required. |
1..n |
At least one value required. |
Cardinality counts [itemprop="<name>"] elements within the item's scope, excluding anything inside nested [itemscope] boundaries.
Type
When type is a primitive URL (https://schema.host/Text, Number, Integer, Boolean, Date, DateTime), each value is format-validated against the rules for that type — see Types.
When type is another schema's governed URL, each value must be a nested [itemscope] with that itemtype, and the nested item is validated recursively against its own schema.
When type is omitted, no type validation runs and any string is accepted.
Defaults
A static default is a literal value, injected as <meta itemprop="..." content="..."> when the property is absent on a write:
<meta itemprop="default" content="draft">
A dynamic default is an expression evaluated at write time, written in either Sessel or JavaScript. The language is selected by the itemtype URL on the wrapper:
<div itemprop="default" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">String.random(8)</script>
</div>
<div itemprop="default" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script itemprop="source" type="module">
export default () => String(Math.floor(100 + Math.random() * 500));
</script>
</div>
Dynamic defaults run server-side, before validation. The Sessel form is a bare expression; self is unavailable because the item does not yet contain the property. The JavaScript form is an ES module whose default export is a function; this is bound to the in-progress instance and a context object is passed as the first positional argument. See JavaScript bindings for the full JavaScript contract.
Child defaults override parent defaults for the same property name.
The same JavaScript/Module source is also used by the pagelove.mjs client-side runtime when discovering schemas in the browser — see Schema definitions in HTML.
Uniqueness
unique: "true" declares individual uniqueness — no two items of this governed type on the host may have the same value for this property.
Any other string is treated as a composite-uniqueness group name. Properties sharing the same group name form a composite key: the combination of their values must be unique across items, though individual values may repeat.
<meta itemprop="unique" content="org-role">
A property can participate in both individual and composite uniqueness:
<meta itemprop="unique" content="true">
<meta itemprop="unique" content="email-org">
Uniqueness is enforced atomically with the write. If any schema in the inheritance chain declares a property as unique, it is unique for the leaf type.
References and cascade
references declares a foreign key. The value format is {itemtype}#{itemprop}, pointing at a property that is declared unique: true on the target type. Every value of the referencing property must match an existing value on some item of the target type.
<meta itemprop="references" content="https://example.com/Org#identifier">
<meta itemprop="cascade" content="restrict">
cascade value |
Behaviour on delete of the referenced value |
|---|---|
"true" |
Cascade: delete referencing items (or clear the reference based on cardinality). |
"restrict" |
Block the operation. A restrict-blocked DELETE returns 409 Conflict. |
omitted / "false" |
No cascade behaviour. |
If several referencing types point at the same source property and any one of them uses restrict, every cascade rule for that source is upgraded to restrict. Data integrity is never silently violated.
What triggers a cascade
A cascade fires when the referenced value disappears or changes through a schema-valid operation:
DELETEof the document holding the referenced value.- A write that removes or changes the referenced value, where the resulting document still satisfies its own schema.
Removing the only value of a 1..1 property is not schema-valid — the document would be left violating its own cardinality — and is rejected with 422 Unprocessable Content before any cascade is considered. The SQL analogue: cascade is a referential action on deleting the row, not on setting a NOT NULL key to NULL. To remove a 1..1 referenced value and cascade, delete the whole document. For 0..1 / 0..n referenced properties, removing the value element (e.g. a selector DELETE) is schema-valid and triggers the cascade.
What the cascade does to referrers
When a cascade fires, each referencing item's disposition is determined by the referencing property's cardinality:
| Referencing cardinality | Action |
|---|---|
1..1 |
The referencing document is deleted. |
0..1 / 0..n |
The referencing property element is removed; the document survives. |
1..n |
The matching value element is removed; if it was the last one, the document is deleted. |
When the referenced value changes (rather than disappears), cascade: "true" rewrites the referencing property values to the new value.
Primary key
@key: "true" declares that this property's value is the schema's primary key. When an instance is materialised via Pagelove.PUT, the property's value is written as the id attribute on the instance's root element.
<li itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="hid">
<meta itemprop="type" content="https://schema.host/Text">
<meta itemprop="cardinality" content="1..1">
<meta itemprop="unique" content="true">
<meta itemprop="@key" content="true">
</li>
A Host instance constructed and PUT through Sessel:
let h = new Host { hid: "abc123", hostname: "abc123.example.com" };
Pagelove.PUT(h, "/hosts/abc123.html")
Persists with the keyed id on the root:
<div id="abc123" itemscope itemtype="https://example.com/Host">
<meta itemprop="hid" content="abc123">
<meta itemprop="hostname" content="abc123.example.com">
</div>
Requirements
| Condition | Behaviour |
|---|---|
@key: "true" AND unique contains "true" |
Honoured. |
@key: "true" on a property that is not individually unique |
Ignored. |
Composite unique="<group-name>" only (no individual "true") |
Does not qualify — same as above. |
Two or more properties on one schema declare @key: "true" |
Document order wins. |
Schema's own properties have no @key; parent schema declares one |
Parent's @key applies (inheritance). |
No @key anywhere in the inheritance chain |
No id is set on the materialised root; current behaviour preserved. |
Value rules
The keyed value is emitted verbatim as the id attribute. Two value-shape rules apply at PUT time:
| Value shape | Behaviour |
|---|---|
| Non-empty, no whitespace | Emitted as id="<value>". Any pre-existing id on the root is overwritten. |
| Empty | No id is emitted. |
| Contains whitespace | No id is emitted (whitespace breaks #id selectors). |
| Property is a computed property | No stored value at materialisation time; no id is emitted. |
Inheritance
@key is inherited from parent schemas. The resolver walks the schema's own properties first (document order, first wins on duplicates), then the parent's properties, and so on up the chain. A child schema can declare its own @key to override the parent's.
Auto-generated default
A property marked @key="true" with no explicit default automatically gets a Sessel default that produces an 8-character value at instance construction. The first character is always a lowercase letter ([a-z]) and the remaining 7 are lowercase-alphanumeric ([a-z0-9]) — this guarantees the value is a valid CSS #id selector (CSS identifiers cannot start with a digit). So:
<li itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="hid">
<meta itemprop="type" content="https://schema.host/Text">
<meta itemprop="cardinality" content="1..1">
<meta itemprop="unique" content="true">
<meta itemprop="@key" content="true">
</li>
Behaves as if you had written:
<li itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="hid">
<meta itemprop="type" content="https://schema.host/Text">
<meta itemprop="cardinality" content="1..1">
<meta itemprop="unique" content="true">
<meta itemprop="@key" content="true">
<div itemprop="default" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
String.random(1, { lower: true }) + String.random(7, { lower: true, digits: true })
</script>
</div>
</li>
new Host {} (no hid) materialises with a random hid like "k3p9zx2m" (always letter-first), the cardinality 1..1 is satisfied, and Pagelove.PUT writes the instance with id="k3p9zx2m" on the root.
To override, declare an explicit default on the property, or pass an explicit value at construction (new Host { hid: "abc123" }).
Computed properties skip the auto-default — they have no stored value to default. Declaring both @key: "true" and a computed binding on the same property has no effect: no auto-default is synthesized, and the property remains computed.
Computed properties
A computed property has no stored value: its value is calculated every time the property is read, from the rest of the instance. Attempting to write a value to a computed property fails — the write is rejected with an error rather than silently accepted.
The current mechanism is an @computed slot, in either Sessel or JavaScript:
<div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="display-name">
<div itemprop="@computed" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
self.first-name + " " + self.last-name
</script>
</div>
</div>
<div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="full-name">
<div itemprop="@computed" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script itemprop="source" type="module">
export default function() { return this.firstName + " " + this.lastName; }
</script>
</div>
</div>
In Sessel, self refers to the whole instance. In JavaScript, this refers to the whole instance (a function form is required — an arrow function does not bind its own this). Either way the expression sees the instance's other properties, not a list of property elements. If a property has both @computed and @read, @computed takes precedence and @read never fires for it. See Resolvers for how @read/@write transformers differ from a computed property, and JavaScript bindings for the JavaScript contract.
Legacy syntax: @read wrapped in bare Sessel
An older, Sessel-only shorthand is still recognized for backward compatibility: an @read slot whose typed item is the bare https://pagelove.org/Sessel type (not Sessel/Lambda) is parsed as a computed property rather than a read transformer:
<div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="display-name">
<div itemprop="@read" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
self.first-name + " " + self.last-name
</script>
</div>
</div>
New schemas should prefer @computed — it works in both languages and doesn't rely on the reader recognizing which of the two Sessel item types is in play.
Validators
@validate is a binding that runs at persistence time (PUT), after defaults have been applied but before the required-property check. In Sessel, self is the property value. In JavaScript, the property value is the first positional argument. The binding must return a truthy value for the write to proceed.
Sessel validator
<div itemprop="@validate" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
self.all((el) => el.text().matches("^[^@]+@[^@]+\\.[^@]+$"))
</script>
</div>
JavaScript validator
<div itemprop="@validate" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script itemprop="source" type="module">
export default (val) => typeof val === 'string' && val.includes('@');
</script>
</div>
When a schema uses inheritance, the most-derived @validate wins. There is no chaining for validators -- a child schema's validator completely overrides the parent's.
The JavaScript form is the same JavaScript/Module typed item used for defaults and resolvers. See JavaScript bindings for the full contract — export default, supported language features, errors, and resource limits.
Error cases
| Condition | Result |
|---|---|
Property with no name |
Silently skipped during parsing. |
| Cardinality violation | 422 with check: "cardinality". |
| Type validation failure | 422 with check: "type". |
@validate returns anything other than true |
422 with check: "@validate". |
@validate fails to compile |
422 on every write to the governed type. |
Malformed references format (missing #, empty type or property) |
Schema load error; every write to the type is rejected. |
references target is not declared unique: true |
Schema load error; every write to the type is rejected. |
| Dynamic default expression fails at write time | Write error on the affected item. |
@key: "true" on a property without unique: "true" |
Schema loads successfully; @key is ignored at resolution. |
Two or more @key: "true" declarations on one schema |
Schema loads successfully; document order wins. |
@key value at PUT time is empty or contains whitespace |
PUT proceeds; no id set on root. |
See also
- Schema — the parent schema item
- Types — primitive types and schema-reference types
- Required combinations — group constraints across several properties
- Resolvers —
@readand@writepipeline details - JavaScript bindings — the server-side JavaScript binding contract for
default,@read,@write, and@validate