Every standard type meets one contract
Every standard type comes with the same baseline of operations, so once you know how one type is built, read, sent, stored and shown, you know them all. Some types add operations of their own on top (Extras beyond the baseline).
| Baseline | What it is | Where a type has none |
|---|---|---|
| Construct and validate | From its parts (PostalCode(raw), Money from money.from_minor), and from text with text.parse(raw, as: T); each failure named by the rule it breaks, malformed for text that isn't the type at all |
Never: every type has both |
format for people |
text.format(value, locale:), giving PresentationText, through CLDR where the locale matters (an address's line order, a name's order, a quantity's unit) |
Every type has one |
| Canonical text | text.from(value): E.164, ISO codes, #rrggbb, ISO 8601, pkg: URLs; the form text.parse reads back |
Types with no single text form (PostalAddress, PersonName); asking is a compile error |
| JSON | One shape, read and written by json.read, json.write and every route |
Never |
| Storage | One shape: a column type, or named columns for an object (Money's two) |
Never |
==, and order |
== on every type; < and sort_by where the type is ordered (quantities of one kind, Money of one currency, times, SemanticVersion, Percentage) |
Order only where it means something |
| Conversions at named edges | geo.from_lng_lat, units.in, Isbn 10 to 13, MaskedCardNumber from a PaymentCardNumber |
Only where the type has another form |
Each of text.parse, text.format and text.from is one operation over
every standard type, as json.read is one over every input type, so the
contract adds 2 names, not 700.
A standard type may take a type argument (Range<T>), be a choice of
other types (Discount, an enum with data),
be built on a built-in type with bounds of its own (TaxRate), or be a
named collection (RichText a list, LocalisedText a map). A named
collection is built, sent and read as one your app declares is. Every
standard object and choice is stored in named columns laid out from its
parts (Every boundary through the contract).
jadpo suggest uses each standard type's shape and its signals, such as
conversion constants, the words of names, format literals and validation
texts, to find code that hand-rolls the type
(Suggestions).
In this example a form sends a quantity as text, an order's time goes into a log line as its canonical text, and a day, a duration and a zone are shown to people in their own words:
type OrderedAt = Instant {}type LogText = Text {}type FormText = Text {}type Quantity = Int { min: 1 max: 12}type QuantityUnreadable = Failure { kind: FailureKind.invalid_value}type QuantityTooLarge = Failure { kind: FailureKind.invalid_value}/// The quantity a form sent: text that may not be a number at all/// (`malformed`), or a number outside `Quantity`'s rules.function quantity_from(raw: FormText) fails QuantityUnreadable, QuantityTooLarge -> Quantity { var quantity: Quantity = attempt text.parse(raw, as: Quantity) { malformed: QuantityUnreadable invalid: QuantityTooLarge } return quantity}/// When an order was placed, as a log line writes it.function placed_text(at: OrderedAt) -> LogText { return LogText(text.from(at))}
test "standard values are read, written and shown one way" { var ordered_quantity: Quantity = attempt quantity_from("4") assert ordered_quantity == 4 assert quantity_from("13") fails QuantityTooLarge assert quantity_from("four") fails QuantityUnreadable assert placed_text("2026-10-25T09:30:00Z") == "2026-10-25T09:30:00Z" assert text.from(Zone.europe_london) == "Europe/London" assert text.format(Weekday.monday, locale: Locale.fr_fr) == "lundi" assert text.format(90m, locale: Locale.en_gb) == "1 hour, 30 minutes" assert text.format(Locale.fr_fr, locale: Locale.en_gb) == "French (France)"}
Extras beyond the baseline
Many groups of types have operations beyond the baseline. Each has its full table under Operations:
| Group | Extras | Count |
|---|---|---|
| Money and payments | money.from_minor, of, minor, currency, sum, allocate, split, convert (with a given ExchangeRate, never fetched), mask (a MaskedCardNumber from a PaymentCardNumber), apply_discount (a Discount off a Price) |
10; arithmetic by the operator table |
Physical quantities, DataSize, Bitrate |
units.of, in, sum; operators within a kind and the defined products and quotients |
3 |
| Time and calendar | The 21 from Configuration and time, plus temporal.occurrences, temporal.open_at and temporal.period_between; CalendarPeriod arithmetic by the operator table |
3 new |
Ranges (Range<T>, DateRange, InstantRange) |
range.contains, range.overlaps, range.intersection |
3 |
| Colour | colour.from_hsl, from_oklch, mix, with_lightness, contrast (WCAG 2) |
5 |
| Security | crypto as tabled below, with sign, verify_signature, verify_jwt and verify_totp |
10 |
| Web | web.with_query(url, entries), web.resolve(base, relative), web.cidr_contains(range, address) |
3 |
| Software | semver.satisfies(version, range) |
1 |
| Media | media.fit(dimensions, within:), keeping the aspect ratio |
1 |
| Text formats | markup.to_html, rich_to_html, to_plain_text; text.pick with fallback. Html is always safe: its construction sanitises |
4 |
| Geometry | geo.distance, length, bearing, area, intersects, contains, within, centroid, bounding_box, the constructions and WKT's two |
Geospatial |
AI (Vector, Embedding, ChatMessage) |
ai.generate, stream, embed, embed_all, distance |
AI |
| People, places, identity numbers, commerce codes, health, travel, other | Baseline only: each identifier's check digit or pattern is its construction |
Rules across a type's parts
Some standard types have parts that constrain each other: a range's start can't be after its end. Each such constraint is a named rule, as on an object type you declare (Rules across fields). So a value you build in code is proved or attempted exactly as a value of your own type is. Here are each type's rules, in the order they are checked, which is also the order OpenAPI's description lists them in:
| Type | Rule | What it holds |
|---|---|---|
InstantRange, DateRange, Range<T> |
start_not_after_end |
start <= end; an open end of a Range<T> keeps it |
IsoWeek |
week_in_year |
week 53 only in a year with 53 ISO weeks |
CalendarEvent |
days_or_times |
both days and no start, end or zone, or all three and no days |
last_day_not_before_first |
last_day >= first_day |
|
end_after_start |
end > start |
|
recurrence_from_start |
the recurrence starts on first_day, or at start in zone |
|
PostalAddress |
postal_code_of_country |
postal_code, where it has one, in its country's form |
IdentityDocument |
number_of_kind_and_country |
number in the form of its kind from its country |
PassportNumber, DrivingLicenceNumber, TaxId, VatNumber |
number_of_country |
number in its country's form |
ContentRating |
rating_of_scheme |
rating one of its scheme's categories |
Timecode |
frames_within_rate |
frames below the rate's whole frames a second |
drop_frame_rate |
drop-frame only at 30000/1001 and 60000/1001 | |
frame_not_dropped |
never a label drop-frame counting skips | |
SubtitleCue |
start_not_negative |
start >= 0ms |
end_after_start |
end > start |
|
text_without_arrow |
text holds no --> |
|
Rating |
best_above_worst |
best > worst |
value_on_scale |
worst <= value and value <= best |
|
BloodPressure |
diastolic_below_systolic |
diastolic < systolic |
MapTile |
within_zoom |
x and y each below 2 to the zoom |
TokenUsage |
cached_within_input |
cached <= input, where it is given |
PasswordPolicy |
min_not_above_max |
min_length <= max_length |
BoundingBox |
south_not_above_north |
south <= north |
Polygon |
rings_valid |
each ring closed and simple, no rings touching, holes inside the outer ring, every edge under half the globe (Polygons are valid when built) |
altitudes_agree |
every position with an altitude, or none (Altitudes are all or nothing) | |
LineString, MultiPoint, MultiLineString, MultiPolygon |
altitudes_agree |
every position with an altitude, or none, a multi-geometry's parts with each other (Altitudes are all or nothing) |
Here a slot built from two bookings' instants is attempted by its rule,
and one whose order a condition has already checked needs no attempt:
type SlotBackwards = Failure { kind: FailureKind.invalid_value}/// A slot from one booking's start to another's end.function combined_slot( earlier_booking: InstantRange, later_booking: InstantRange) fails SlotBackwards -> InstantRange { return attempt InstantRange { start: earlier_booking.start end: later_booking.end } { start_not_after_end: SlotBackwards }}/// The same, refused before it is built.function combined_slot_checked( earlier_booking: InstantRange, later_booking: InstantRange) fails SlotBackwards -> InstantRange { if (later_booking.end < earlier_booking.start) { reject SlotBackwards } return InstantRange { // the condition proves `start_not_after_end` start: earlier_booking.start end: later_booking.end }}
test "a slot never ends before it starts" { var morning_slot: InstantRange = InstantRange { start: "2026-10-26T09:00:00Z" end: "2026-10-26T12:00:00Z" } var afternoon_slot: InstantRange = InstantRange { start: "2026-10-26T13:00:00Z" end: "2026-10-26T17:00:00Z" } var whole_day: InstantRange = attempt combined_slot(morning_slot, afternoon_slot) assert whole_day.end == afternoon_slot.end assert combined_slot(afternoon_slot, morning_slot) fails SlotBackwards assert combined_slot_checked(afternoon_slot, morning_slot) fails SlotBackwards}
- Each rule behaves like a condition you write. It is named, proved,
attempted and reported exactly as a condition is. It runs on every
value made, however it is made: an object literal,
text.parse, a request's JSON orjson.read, in compiled code as in the interpreter. The checker reads a literal by the same check. Every check across a standard type's parts is one of these rules, so there is no second mechanism. - How the checker proves a rule. It reads a literal by what it shows
of each part. A part written out is known, a part written
noneis absent, and a part built from a value of a type that can't benoneis given. A rule its parts decide either holds or is the errortype.invalid_literal, naming the rule. - A rule ordering two parts (a range's ends, a rating's, a password policy's lengths) can otherwise be proved as a condition comparing two fields is: by a comparison known where the value is built, or by the declared ranges. A password policy's minimum length of 8, for example, is never above its maximum, which is at least 64. Any other rule on a value built from other values is attempted.
- A rule may read one part. A subtitle cue's start at 0 or later, and
its text without
-->, are named rules checked in the cue's order, not bounds the part's value would be built against on its own. Where one statement covers it, it is one rule: a rating's value from its worst to its best isvalue_on_scale, and a map tile's column and row below 2 to the power of its zoom iswithin_zoom. - Text and JSON break rules by name. Text in the type's form that
breaks a rule (week 53 of 2027, a cue ending before it starts,
PEGI 12A) is read and then breaks the rule; it isn'tmalformed. Only text that isn't the type at all ismalformed. Soattempt text.parse(raw, as: T)maps each ofT's rules, orinvalid:. JSON reports the rule at the object's path, or at the field's path for a type sent as its text. - Text written where such a type goes is refused. That text would be
the type's canonical text, which is read only when the program runs.
The checker doesn't have the readers the program uses for it, so it
couldn't check the rules. The literal is
type.invalid_literal: write the value out as an object literal, whose rules are checked, or read the text withtext.parseandattempt. A type with one text form and no such rules (SemanticVersion,CardExpiry,Colour) still takes its text. - Any other literal where a standard object type goes is
type.mismatch: a number, aBool, or text where a type with no single text form goes (PostalAddress, or WKT where a geometry goes, mixed altitudes included). - A value read from the store is checked against its rules, as a value made anywhere else is. Jadpo never writes a value that breaks one, so a row that does was written by something else. Reading it is a defect naming the rule, never the values. A rule the running program can't decide, such as one on a part of the wrong kind, counts as broken.
- OpenAPI describes a standard object's schema with "Rules across its
parts:" and each rule.
Range<T>is an object ofstartandend, each its item ornull, with its rule in its description.
A sale's dates written as a DateRange's text can't be checked while
compiling:
/// When the shop's winter sale runs.function winter_sale() -> DateRange { return "2026-12-27/2027-01-03"}
error: `"2026-12-27/2027-01-03"` can't be checked as a `DateRange` while compiling --> main.jadpo:3:12 |3 | return "2026-12-27/2027-01-03" | ^^^^^^^^^^^^^^^^^^^^^^^ canonical text reason: A `DateRange` has rules across its parts, which every literal is checked against while compiling; text written where one goes is its canonical text, which isn't read until the program runs, so its rules can't be checked. next: Write it out as `DateRange { ... }`, or read the text with `text.parse(..., as: DateRange)` and `attempt` (agent) rule: type.invalid_literal help: https://jadpo.dev/docs/errors/type.invalid_literalcheck failed: 1 error, 1 file
Written out, the sale's rules are checked while compiling:
/// When the shop's winter sale runs.function winter_sale() -> DateRange { return DateRange { start: "2026-12-27" end: "2027-01-03" }}
Markdown for agents: /docs/every-standard-type-meets-one-contract.md.Built from Jadpo main at a707efb.