Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

Every boundary through the contract

On this page

A standard value keeps its currency, unit and precision wherever it goes into or out of your program. Every place a value crosses encodes a standard type only through the contract's JSON, storage and text shapes, never through an encoder of its own:

Boundary Encoding Specified in
HTTP requests and responses, OpenAPI The contract's JSON, through the route codec HTTP, this page
Storage and migrations The contract's storage: one column, named columns laid out from the type's parts, or a native PostgreSQL type (a range, time, timestamp); a quantity's column carries a comment naming its SI unit; a fixed currency or rate a check constraint Data, this page
SQL literals: a query's or a lifecycle's condition, a transition's set:, a backfill The value the storage codec makes, which the runtime binds: a parameter wherever the statement takes one, otherwise that value written as SQL text Data and Reads and writes, this page
Caches and Redis entities The contract's JSON, as Caches holds the API's JSON there Data
Event, job and batch payloads The contract's JSON of the event's or the job's type, written and read by the route codec, and a batch's captured values the same; read with the type as it is when delivered, so a change an API record would call breaking needs an acceptance, and a payload that no longer reads is parked, never dropped Jobs and events
CSV and TSV cells A standard type's canonical text (text.from), read by text.parse; a type with no single text form (PostalAddress, Price, a choice) is the compile error naming JSON Lines that a list or an object is This page, Data formats
JSON Lines rows, json.read and json.write The contract's JSON This page
Logs and traces Values as Logging and Personal data allow; a personal value's keyed hash covers the UTF-8 of its canonical text, or of its contract JSON where it has no single text form Personal data
Settings text.parse's canonical text, with the source spellings a setting also takes (5m) Settings
Translation placeholders and email Shown to people by text.format (PresentationText), never read back Configuration and time and Built-in services
Outbound provider calls and webhooks The contract's JSON for a field without wire:; wire: only names another encoding a provider uses Integrations
Records and the manifest Types and defaults as source writes them; no runtime values Records and manifest
Test fixtures and seeds Written through the actions they call, so through storage Data and Testing
Files Bytes; the format namespaces above turn them into values Files
Developer reports A value as code makes one (money.from_minor(1050, Currency.gbp)), which keeps its currency, unit and places and is never read back Debugging and review
The interpreter and compiled code, native and wasm32 One runtime crate, so one codec for each shape The compiler and runtime

Your app's own structured values are stored as their contract's JSON too (Structured values).

Storage layout

Every standard type is stored in one layout, worked out from its parts:

  • An object is its parts in order, each in as many columns as it takes. An amount of money takes two, its minor units and its currency's code. Another standard object or choice takes its own columns, and a list of text or codes takes one text[] column. A ZonedTime is two columns, its instant and its zone.
  • A choice is its variant's name in a kind column, then each field its variants use, once, in the order they first appear. Two variants that use one field name share its column (a UnitQuantity's amount, at the finest scale any kind keeps). A column is NULL where the stored variant doesn't have that field.
  • Native where PostgreSQL has the type, in one column named for the field (a suffix of ""): ranges in daterange, tstzrange (an InstantRange), numrange and int8range (a Range<Duration> in milliseconds), a LocalTime in time, a LocalDateTime and an event's local times in timestamp without a zone, a Year and a UtcOffset in smallint. A CalendarPeriod is three integer columns, never an interval (Time and calendar additions).
  • A range that holds nothing isn't stored. PostgreSQL keeps every range whose start is its end and that doesn't hold its end as empty, without its ends, so it couldn't be read back. Storing one is a fault that says so, and reading empty is one too. A DateRange of one day holds that day, so it is stored. The compiler doesn't prove at compile time that a stored range isn't empty.
  • Every standard enum is stored by its code, as Currency is: Zone as Europe/London, Locale as en-GB, HttpMethod as GET, FileType as image/png.
  • A quantity's column carries a comment giving its kind and SI unit, as the units table writes it: Length in metres (m). A column that a choice's variants share names each kind's unit with the variant that holds it. A migration sets the comment with the column and changes it with the type, and the migration snapshot records it.
  • A fixed currency is a check constraint on each column a type fixes, by the setting that fixes it: a Money type's currency:, and a rate type's from: and to: (<table>_<column>_from_check). So a stored rate's currencies are always its type's.
  • An empty value in a field that can't be none reads back as that value: a ProblemDetails that says nothing, whose parts may all be none, comes back as an empty ProblemDetails. In a field that may be none, every column NULL is none, so an empty ProblemDetails there reads back as none.
  • A part that is a built-in type held as a standard type is read as that type, as OidcClaims' email is read as an Email.
  • JSON-held values. RichText, RichTextBlock (a choice whose variants hold lists of objects) and LocalisedText are one jsonb column each, as your app's structured values are. The column holds exactly what json.write writes for the value, and is read back as json.read reads it, with an Input's rules. PostgreSQL keeps jsonb in its own key order and spacing, so the stored text isn't byte for byte what was written, but it reads back ==. Ordering a query by a JSON-held field is refused.
  • What doesn't read back is a fault, as any column that doesn't make its type is, named by its field's path and the rule it broke.

Faults and errors never carry a value. A value a column can't hold, a stored code that isn't one of its enum's, a decimal or a time the PostgreSQL wire can't carry, and PostgreSQL's own message for a value it can't take (its class 22, whose message may quote the value) are each reported by what the value is and the rule it broke, as a DecodeProblem names the field and the rule. A statement's parameter doesn't know its field, so the field isn't named there.

Text at a boundary

Text at a boundary is read one way, wherever it arrives:

  • One reader per text form. A built-in type's text is read by one reader, which text.parse, a JSON string, a path, a query string, a header, a setting and a map's key all use:

    • an Instant ends in Z, also as a ZonedTime's instant in JSON and as an InstantRange's ends;
    • an Int is digits, with a - when negative;
    • a Decimal has places after a point, with digits on both sides;
    • a duration's days are digits.

    Text is composed to NFC first. So an Int sent as "+5", a Decimal written 1_000, .5 or --5, a duration of days with a sign (P+1D) and a whole-number standard type written -0 are refused.

  • No request input carries an offset. Every instant on the wire ends in Z ("2026-01-15T12:00:00Z"), as HTTP and Time types say. An offset is only a way to write an Instant literal in source, so a request, a setting and json.read refuse one (2026-10-25T10:30:00+01:00), also as a ZonedTime's instant, breaking format.

  • A place that holds text reads the canonical text of every standard type that has one. A map's key is a Money, a quantity, a ZonedTime, a Range or a standard object sent as its fields, written as text.from writes it. A whole number with a text form of its own (HttpStatus, BasisPoints) is read as text.parse reads it, so -0 isn't one.

  • No object in a URL. A query string and a path's placeholder hold no object (The query string, Paths and placeholders). The contract's JSON sends an amount of money, a quantity and a ZonedTime as an object, so each is route.query_field_unsupported or route.placeholder_type there, as an object is. A standard object with a canonical text stays out of a query string too, since a query string takes only scalars and enums.

  • A setting holds the types What a setting can hold lists, read by text.parse's canonical text. The table's "Settings" row names their encoding; it doesn't add types. A Duration setting reads 5m and PT5M alike.

  • A standard named collection crosses a boundary as its type allows: type.map_key_boundary doesn't refuse a LocalisedText, whose keys are locales' tags, in an Input or an Output.

JSON at a boundary

Every standard type's JSON has one encoder:

  • Each type's JSON is read and written from one description of it. Money, ZonedTime and every quantity are sent as an object of the members their type lists, each a kind of member: a whole number of minor units, a standard enum's code, an instant, a decimal written as text, or one of the kind's units. OpenAPI describes it from the same list.
  • A part is described as its type. In OpenAPI, a standard object's part that is a standard type a built-in type holds is that type's schema: OidcClaims' email is a string of format: email and its picture one of format: uri.
  • Zones and sign counts. A ZonedTime's zone is read ignoring case, as a Zone is, and OpenAPI lists its zones by name, as it does a Zone's. A WebAuthnCredential's sign_count is a JSON number.
  • A JSON object that names a member twice isn't a document Jadpo reads. json.read gives malformed, and a request's body is refused as any body that isn't JSON is, whether the object is an Object's, an Input's or a map's. Another reader might keep the other value, so the value read could be one the sender didn't mean; crypto.verify_jwt refuses a token whose claims do the same. A set's repeated item stays a duplicate problem, since it is a value, not the document's shape.
  • The reader is Jadpo's own: RFC 8259's grammar as serde_json reads it, with the same 128 levels of nesting. It reads every number's double correctly rounded, and keeps each number's text as written beside the document.

SQL literals

A literal in SQL is the value the storage codec makes for it:

  • The literal is decoded for its field first, exactly as a value in a request body is: "2026-10-25" where a CalendarDate goes is the date, 5 where scale: 2 goes is 5.00, 20% is the fraction 0.20, 2.5km its metres, and Currency.gbp the code GBP. The same codec then makes both the runtime's parameters and the compiler's literals.
  • A parameter wherever the statement takes one. A query's, a get's and an include's conditions, a lifecycle's visible: and when:, a transition's set:, a create's initial:, a purge, and a membership's roles in policy bind each literal and enum value as a parameter, the value the codec made. PostgreSQL plans an unnamed statement with its parameters' values, so a bound visible: still proves a partial index's predicate. jadpo sql shows a bound literal's parameter as SQL writes its value: $3: 'live' · text.
  • Written as SQL text only where a statement takes no parameters: a partial index's predicate (DDL), a migration's backfill (its postgres.sql runs as written), and the identity statements whose parameters are fixed (a passkey's, a session's and a service key's lookup, for the caller's active: and its policy). The compiler's own text, a column's comment and the codes a check constraint names, is escaped the same way.
  • How the SQL text is written. It is PostgreSQL's spelling of the codec's value, cast where the bare spelling could read as another type:
    • text is quoted, with each ' doubled. When it holds a backslash or an ASCII control character, it is an escape string (E'...') with each backslash doubled and each control character escaped. So standard_conforming_strings can't change its meaning, and it stays on one line: a migration's sections are read line by line, so a \r would be dropped, and a line of text starting -- store: would be read as a section of its own;
    • an instant is written in UTC with +00, so the session's TimeZone can't change it;
    • a number is written only when it is digits, a sign and a point.
  • Text holding NUL is never stored. PostgreSQL text can't hold the NUL character: a backfill holding it is migration.decision_invalid, a predicate holding it isn't written (the index holds every row), and a condition binds it, which the store refuses as it refuses a runtime value holding one.
  • A backfill takes every literal its field's type has, an instant, a date, a duration, a quantity and a percentage included, each checked as any literal of its type is, so a standard type's own check (an email address's) refuses a backfill that breaks it.
  • A literal is never absent, so comparing an optional field with a literal in a where: line, or testing it in a list of literals, compiles; it isn't query.optional_value.

Comparing a JSON-held field in a query's condition isn't built yet: it is toolchain.not_built_yet, as a condition on a compound type is.

Markdown for agents: /docs/every-boundary-through-the-contract.md.Built from Jadpo main at a707efb.