Objects
An object is a fixed set of named fields. The base after = says what the
object is for:
type StockTag = Object { // used inside the application amount: Stock}type CreateProduct = Input { // arrives from outside, such as a request body name: ProductName stock: Stock}type ProductView = Output { // sent back, such as a response name: ProductName stock: StockTag}
Only an Input may leave fields out or give them defaults. A route accepts
only an Input and returns only an Output. There is no unnamed object: a
nested shape is declared once, with a name, and used by that name.
An object value is written with its type name and every field:
function view(name: ProductName, stock: Stock) -> ProductView { return ProductView { name: name stock: StockTag { amount: stock } }}
Every declared field must be given, and unknown fields are errors. Fields can hold other objects and lists, nested as deeply as needed.
An object literal never fails on a field. A field accepts its declared type or anything
that widens to it, and when the field adds rules of its own, the declared
ranges must prove the value follows them (type.field_unproved). A value
that might break them is built first, on its own line, as the field's type
(below), where its failure is mapped. Only a rule the type writes across
its fields can make a literal fail ("Rules across fields", below):
type Badge = Object { name: Text { max_length: 12 }}function make_badge(profile: Profile) fails BadgeNameTooLong -> Badge { var name: Badge.name = attempt Badge.name(profile.display_name) { max_length: BadgeNameTooLong } return Badge { name: name }}
Names from reserved words
A reserved word (Reserved words)
may name a field in the body of a type or entity, and a variant in the body
of an enum. Other languages' data is full of a field called type, and
nothing here can be misread: a field is declared as word: Type, set as
word: value and read as thing.word, and a variant is always written
after its enum's name.
type Kind = Enum { library package type}
use depot/types { ParcelName }type Haulier = Enum { public test: { for: ParcelName }}
use depot/types { Kind, ParcelName }entity Parcel { id: Uuid package: ParcelName type: Kind in: ParcelName? policy: { create: [Access.public] read: [Access.public] }}
use depot/entities { Parcel }use depot/types { Kind }/// A parcel as clients see it.type ParcelView = Output { pick: [Parcel.package, Parcel.in] type: Kind}
use depot/entities { Parcel }use depot/types { Kind, ParcelView }query parcels_of(name: Parcel.package, asked: Kind) -> List<ParcelView> { from: Parcel where: Parcel.package == name where: Parcel.type == asked order: [Parcel.in asc] limit: 20}
use depot/entities { Parcel }use depot/types { Haulier, Kind, ParcelView, Waybill }use depot/libraries { notices }/// What shipping a parcel takes.type ShipParcel = Input { package: Parcel.package type: Kind}action ship_parcel(input: ShipParcel) -> ParcelView { var parcel: Parcel = Parcel.create { set: { package: input.package type: input.type in: notices.label( Waybill { if: input.package haulier: Haulier.public } ) } } return ParcelView { package: parcel.package type: parcel.type in: parcel.in }}
use depot/types { Haulier, ParcelName }type Waybill = Object { if: ParcelName haulier: Haulier}
use depot/types { Haulier, ParcelName, Waybill }library notices { /// Who a parcel is for. function label(waybill: Waybill) -> ParcelName { return match waybill.haulier { Haulier.public -> waybill.if Haulier.test(details) -> details.for } }}
use depot/types { ParcelView }use depot/actions { ShipParcel, ship_parcel }routes Parcels { prefix: /parcels /// Ships a parcel. POST / { authentication: RouteAuthentication.none input: ShipParcel output: ParcelView status: SuccessStatus.created run: ship_parcel(input) }}
// Generated by Jadpo from the routes of API main; update it with `jadpo api update`, never by hand.record api 1GET /openapi.json generated openapi authentication none status 200POST /parcels authentication none input ShipParcel (body) output ParcelView status 201enum Kind variant library variant package variant typeoutput ParcelView in: Text { min_length: 1, max_length: 80 }? package: Text { min_length: 1, max_length: 80 } type: Kindinput ShipParcel package: Text { min_length: 1, max_length: 80 } type: Kind
- What holds it says what it is. A field is read as
thing.type, a query's condition names it asParcel.type, and a wording or amatcharm names a variant asKind.library. - A list of fields may name it alone, as
unique: { both: [package, type] }, a policy'sread: [for]andorder: [type desc]do, since nothing else a list holds is one word and nothing more. - Elsewhere, alone, the word is still the word. A rule across fields names
fields alone, so it can't read one named with a reserved word:
rule r: type == kindissyntax.expected_name, and the field is renamed or the rule moved into a function. A field callednoneortrueis the value wherever it stands alone, in a list too. - Only fields and variants. A parameter, a path parameter, a
matchbinding, a variable, a declaration, a rule, aunique:entry and a setting the language doesn't define still can't be named with one. - No enum has a
true,falseornone.Kind.nonebeside aKind?would read as thenoneit isn't, so a variant called one of these istype.reserved_variant_name, asallis. A field may be called one. - The wire has the name as written. Records, OpenAPI and JSON all say
type, with no spelling of their own, and a column is quoted like any other.wire_name:stays for provider packages, where the provider's spelling isn't the Jadpo name (Spellings). include,pick,omitandoptional, which aren't reserved words, build a type's fields and staytype.reserved_field_name.
Field types
Every field of an object, entity or enum variant is a type of its own, written
Owner.field. Customer.email is a refinement of Email: it can be used
wherever an Email is expected, but an Email is not automatically a
Customer.email, and Supplier.email is never one.
entity Customer { id: Uuid email: Email}entity Supplier { id: Uuid email: Email}function contact_address(email: Email) -> Email { return email}function customer_contact(customer: Customer) -> Email { return contact_address(customer.email)}function supplier_contact(supplier: Supplier) -> Email { return contact_address(supplier.email)}
Field types are how identifiers stay apart: Customer.id and Supplier.id
are both UUIDs, and passing one where the other is expected is
type.sibling_mismatch. To move a value into a field type on purpose, use the
field as a constructor, as in Customer.email(email).
A field can add rules of its own in a block after its type:
type Badge = Object { label: ProductName { max_length: 12 }}
Reading through a nested object gives the inner object's field type:
customer.billing_address.postal_code has type Address.postal_code, whether
it came from the billing or the shipping address.
A field's type is always a type: a type's name, a field type such as
Customer.email, a package's type, List<T>, T?, or a type with rules in
a block. A value written there, whether a call (TicketStatus("open")), a
literal or an enum's variant (TicketStatus.open), is type.not_a_type,
naming the field, in every block of fields: an object's, an input's, an
event's, a failure's, a variant's, an entity's and config { }'s settings.
Rules across fields
A field's rules say what one value may be. A rule across fields says how
the values of one object relate: a booking ends after it starts, a
discount is below the price. An Object, Input or Output names each
such rule in its block, and the rule is checked whenever a value of the
type is made.
type Booking = Object { starts: Moment ends: Moment rule ends_after_start: ends > starts}function book(starts: Moment, ends: Moment) fails BookingBackwards -> Booking { var booking: Booking = attempt Booking { // nothing proves the rule starts: starts ends: ends } { ends_after_start: BookingBackwards } return booking}function book_checked(starts: Moment, ends: Moment) fails BookingBackwards -> Booking { if (ends <= starts) { reject BookingBackwards } return Booking { // the condition proves `ends_after_start` starts: starts ends: ends }}test "a booking that ends before it starts is refused" { assert book("2026-03-02T10:00:00Z", "2026-03-02T09:00:00Z") fails BookingBackwards assert book_checked("2026-03-02T10:00:00Z", "2026-03-02T09:00:00Z") fails BookingBackwards}
- Written in the type's block.
rule name: condition, one per line, usually after the fields. A rule's name is lower_snake_case and its own: no field or other rule of the type has it, and it is neverinvalid, which anattemptkeeps for every rule (type.rule_name_taken).ruleisn't reserved: a field may still be calledrule. - A condition over the type's own fields. It names the fields bare
(
ends) and reads through them (stay.ends); it uses literals, variants, operators,matchand the standard operations that always give a result from their arguments alone. Nothing else: no function or action of the application, constant, setting, clock, random value, key or record (type.rule_outside_fields). Not even a constant: a rule means the same wherever its type is read. It is aBool(type.rule_not_condition). A field that may benoneis read withmatch, as anywhere else. - Checked whenever a value is made, once every field has its value,
whether the object stands alone, as a list's item or as a map's entry. A
value is made by an object literal, a construction,
json.read, a request's body or query string, the output a route test reads,text.parseof a standard type's text, and a read from the store. Jadpo never stores a value that breaks a rule, so one read back from the store is a defect (Entities, stores and migrations). - One rule is reported. The field rules run first, then each rule in the order written, and the first one broken is the one reported, so a report never depends on more than one rule.
- A setting never holds an object, so never one with rules
(
configuration.type_unsupported). - A query's result type has none (
query.result_rule, Reads and writes). A read has noattemptto map a broken rule, so the type is refused rather than each row checked as it is read. - An object literal can break one. Building a type with rules is a
construction, mapped as
Type(value)is (Construction failures by rule). Each rule that neither the declared ranges nor a condition proves needs an arm keyed by its name, orinvalid:covers it. - A literal is checked while compiling, or refused. One whose values
are all literals is worked out while compiling, so a booking written
backwards is
type.invalid_literal. A literal is never kept unchecked: text where a standard object type with rules across its parts goes istype.invalid_literal, since the compiler can't read its text, and any other literal a standard object can't be istype.mismatch(Rules across a type's parts). - Proof reads the values given. A rule is read with each field
replaced by the value given for it, so
ends > starts, built withstarts: beginandends: finish, is proved afterif (finish <= begin) { reject ... }leaves, or insideif (finish > begin). A comparison proves itself between any two values written the same way, instants and dates as well as numbers (Conditions as proof); numbers are proved by their declared ranges too, andand,orandnotcombine as they do in a condition. A rule that calls a standard operation or usesmatchis never proved this way, so a literal that may break it is always attempted. - Every failure is
invalid_value. The report names the rule (ends_after_start), never the values. JSON that breaks one, in a request orjson.read, is a problem at the object's path, empty for the whole input, whose rule is the rule's name (Data formats). - OpenAPI says them in words. OpenAPI can't express most rules, so each object's schema description lists its rules as written (HTTP).
- A rule belongs to the type that writes it.
includeandpickcopy fields, never rules: a type built fromBookingwrites the rule again if it holds there too. Only anObject,InputorOutputhas rules (type.rule_outside_object); events, failures, messages, enums and refinements have none. A rule on an entity, which the database would check and a write would map, is not designed yet. - The standard object types have rules too: an
InstantRange'sstart_not_after_end, aPostalAddress'spostal_code_of_country. Each is named, proved, attempted and reported as a rule written here is. Rules across a type's parts lists them.
In a rule, numbers are proved by their declared ranges as a field's rules
are, and a field that may be none is read with match:
type StaySearch = Input { arrives: StayDate leaves: StayDate adults: GuestCount children: ChildCount { optional: true } rule leaves_after_arrival: leaves > arrives rule children_with_adults: match children { some(count) -> count <= adults * 2 none -> true }}type Party = Object { adults: GuestCount children: ChildCount rule not_crowded: children <= adults * 6}function party(adults: GuestCount, children: ChildCount) -> Party { return Party { // at most 6 children, and at least 6 places: always holds adults: adults children: children }}
A construction whose every rule is proved can't fail, so it takes no
attempt, and the error points at the condition that proves it, as it
does for any other construction:
type Moment = Instant {}type BookingBackwards = Failure { kind: FailureKind.invalid_value}type Booking = Object { starts: Moment ends: Moment rule ends_after_start: ends > starts}function book(starts: Moment, ends: Moment) fails BookingBackwards -> Booking { if (ends <= starts) { reject BookingBackwards } var booking: Booking = attempt Booking { starts: starts ends: ends } { ends_after_start: BookingBackwards } return booking}
error Booking { ... } can't fail, so it takes no attempt
--> main.jadpo:17:28
|14 | if (ends <= starts) { | -------------- checked here17 | var booking: Booking = attempt Booking { | ^^^^^^^ nothing to pass on
- reason
- The declared ranges and the condition
ends <= startsproveBooking { ... }always follows its type's rules, so building it never fails;attemptmarks only what can. - next
- Remove
attempt(agent) - rule
- failure.
attempt_ infallible - help
- https://jadpo.
dev/docs/errors/failure. attempt_ infallible
A record names its type alone
A record's head is its type's name, never type arguments. Range { ... },
Feature { ... } and FeatureCollection { ... }, the standard types that take
them, get them from the type the record's place expects, or else from its
values, so Feature<Point, Pin> { ... } is
type.record_type_arguments.
Markdown for agents: /docs/objects.md.Built from Jadpo main at aee2e8f.