Schema

A Schema declares a governed type — the itemtype URL it applies to and the properties, validators, and constraints that are enforced when items of that type are written or read.

When to reach for it

Declare a schema for any itemtype that needs cardinality enforcement, type validation, defaults, resolvers, uniqueness, referential integrity, or @validate rules. Items whose itemtype has no matching schema are stored unchanged, without any of those checks.

Shape

A schema is an HTML element with itemscope itemtype="https://pagelove.org/Schema" and a type child that names the governed URL. Everything else is optional.

<div itemscope itemtype="https://pagelove.org/Schema">
  <meta itemprop="type" content="https://example.com/Person">
  <meta itemprop="parent" content="https://example.com/Entity">

  <div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
    <!-- See the Property reference -->
  </div>

  <div itemprop="constraint" itemscope itemtype="https://pagelove.org/GroupConstraint">
    <!-- See the Required combinations reference -->
  </div>

  <script type="text/sessel" itemprop="@validate">
    <!-- Schema-level validator -->
  </script>
</div>

Fields

Field Cardinality Accepts Purpose
type 1..1 URL The itemtype URL this schema governs. A schema with no type is silently skipped.
name 0..1 Text Human-readable name. Used in error messages and tooling.
description 0..1 Text Human-readable description of what the type represents.
parent 0..1 URL Governed type URL of a parent schema to inherit from.
property 0..n Nested Property item A property declaration.
constraint 0..n Nested Required combinations item A group-level cardinality rule across several properties.
@validate 0..1 Sessel or JavaScript binding Schema-level validator. Must return truthy. See below.

Inheritance

A schema with a parent field inherits everything the parent declares — properties, group constraints, resolvers, and validators. The chain is walked from root to leaf; a child property with the same name as a parent property overrides the parent's declaration for that property.

When parent is omitted, user-defined schemas implicitly inherit from https://pagelove.org/Instance. Every user schema participates in the Instance type hierarchy unless it declares a different explicit parent.

Cycles are detected and rejected — a cyclic chain produces a 422 on every write to items of any schema in the cycle. An unknown parent URL is not rejected: the inheritance chain silently stops at the last resolvable ancestor, and the schema behaves as if it had no further parent. This is different from a cycle, which is always an error.

How inherited members combine

Inherited member Merge rule
Properties All parent properties are included. Child declarations override parent declarations by name.
Group constraints Parent constraints apply unless the child declares a constraint under the same group name.
@read resolvers Run ancestor-first — parents transform before children.
@write resolvers Run child-first — children transform before parents.
@validate expressions Every validator in the chain runs, ancestor-first. All must return true.
unique If any schema in the chain marks a property as unique, it is unique for the leaf type.

Schema-level @validate

Use schema-level @validate for rules that span more than one property — "start date must be before end date", "total must equal the sum of line items".

Schema-level @validate accepts either a Sessel expression or a JavaScript module. As with the other binding slots, the language is selected by the wrapper's itemtype URL, not by the <script type> attribute.

Sessel

The expression's self binding is the [itemscope] element being written, so self.microdata() gives read access to every property on the item.

<script type="text/sessel" itemprop="@validate">
  let md = self.microdata();
  md["start-date"].first() < md["end-date"].first()
</script>

JavaScript

Wrap an ES module in a JavaScript/Module item. The module's default export must be a function, and the schema-level slot binds the instance to this as a serialized element string (it is not passed as a positional argument; the positional context argument is null for this slot). A function form is required so this is observable; an arrow function in a strict-mode ES module does not bind its own this. Return truthy to accept, falsy or thrown to reject.

<div itemprop="@validate" itemscope itemtype="https://pagelove.org/JavaScript/Module">
  <script itemprop="source" type="module">
    export default function () {
      // `this` is the serialized instance HTML.
      return this.includes('itemprop="start-date"');
    }
  </script>
</div>

Note: the schema-level slot's this-as-serialized-HTML convention differs from a property-level JavaScript @validate, where the property's value is passed as the first positional argument. See JavaScript bindings for the full contract, supported language features, errors, and resource limits.

Schema-level @validate runs only after cardinality, type, and property-level @validate have all passed, and before group constraints are checked. This guarantees per-property structural correctness before the cross-property rule evaluates against it; group constraints (which span properties, the same category of check) run last.

Examples

Minimal

<div itemscope itemtype="https://pagelove.org/Schema">
  <meta itemprop="type" content="https://example.com/Tag">
  <div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
    <meta itemprop="name" content="label">
    <meta itemprop="type" content="https://schema.host/Text">
    <meta itemprop="cardinality" content="1..1">
    <meta itemprop="unique" content="true">
  </div>
</div>

With inheritance and a cross-property validator

<div itemscope itemtype="https://pagelove.org/Schema">
  <meta itemprop="type" content="https://example.com/Entity">
  <div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
    <meta itemprop="name" content="identifier">
    <meta itemprop="type" content="https://schema.host/Text">
    <meta itemprop="cardinality" content="1..1">
    <meta itemprop="unique" content="true">
  </div>
</div>

<div itemscope itemtype="https://pagelove.org/Schema">
  <meta itemprop="type" content="https://example.com/Person">
  <meta itemprop="parent" content="https://example.com/Entity">
  <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">
  </div>
  <script type="text/sessel" itemprop="@validate">
    let md = self.microdata();
    md["email"].first().matches("^[^@]+@[^@]+\\.[^@]+$")
  </script>
</div>

Error cases

Condition Result
Schema with no type Silently skipped during parsing. No writes are validated against it.
Unknown parent URL Not an error — the inheritance chain silently stops at the last resolvable ancestor.
Circular inheritance chain 422 on every write to items of any schema in the cycle.
@validate fails to compile 422 on every write to items of the governed type.
@validate returns anything other than true 422 with check: "@validate" in the violation.

The 422 response body is HTML with Microdata, using the https://pagelove.org/SchemaViolation itemtype.

See also