Every boundary through the contract
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. AZonedTimeis two columns, its instant and its zone. - A choice is its variant's name in a
kindcolumn, then each field its variants use, once, in the order they first appear. Two variants that use one field name share its column (aUnitQuantity'samount, at the finest scale any kind keeps). A column isNULLwhere 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 indaterange,tstzrange(anInstantRange),numrangeandint8range(aRange<Duration>in milliseconds), aLocalTimeintime, aLocalDateTimeand an event's local times intimestampwithout a zone, aYearand aUtcOffsetinsmallint. ACalendarPeriodis threeintegercolumns, never aninterval(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 readingemptyis one too. ADateRangeof 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
Currencyis:ZoneasEurope/London,Localeasen-GB,HttpMethodasGET,FileTypeasimage/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
Moneytype'scurrency:, and a rate type'sfrom:andto:(<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
nonereads back as that value: aProblemDetailsthat says nothing, whose parts may all benone, comes back as an emptyProblemDetails. In a field that may benone, every columnNULLisnone, so an emptyProblemDetailsthere reads back asnone. - A part that is a built-in type held as a standard type is read as
that type, as
OidcClaims'emailis read as anEmail. - JSON-held values.
RichText,RichTextBlock(a choice whose variants hold lists of objects) andLocalisedTextare onejsonbcolumn each, as your app's structured values are. The column holds exactly whatjson.writewrites for the value, and is read back asjson.readreads it, with anInput's rules. PostgreSQL keepsjsonbin 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
Instantends inZ, also as aZonedTime's instant in JSON and as anInstantRange's ends; - an
Intis digits, with a-when negative; - a
Decimalhas places after a point, with digits on both sides; - a duration's days are digits.
Text is composed to NFC first. So an
Intsent as"+5", aDecimalwritten1_000,.5or--5, a duration of days with a sign (P+1D) and a whole-number standard type written-0are refused. - an
-
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 anInstantliteral in source, so a request, a setting andjson.readrefuse one (2026-10-25T10:30:00+01:00), also as aZonedTime'sinstant, breakingformat. -
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, aZonedTime, aRangeor a standard object sent as its fields, written astext.fromwrites it. A whole number with a text form of its own (HttpStatus,BasisPoints) is read astext.parsereads it, so-0isn'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
ZonedTimeas an object, so each isroute.query_field_unsupportedorroute.placeholder_typethere, 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. ADurationsetting reads5mandPT5Malike. -
A standard named collection crosses a boundary as its type allows:
type.map_key_boundarydoesn't refuse aLocalisedText, whose keys are locales' tags, in anInputor anOutput.
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,ZonedTimeand 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'emailis a string offormat: emailand itspictureone offormat: uri. - Zones and sign counts. A
ZonedTime'szoneis read ignoring case, as aZoneis, and OpenAPI lists its zones by name, as it does aZone's. AWebAuthnCredential'ssign_countis a JSON number. - A JSON object that names a member twice isn't a document Jadpo
reads.
json.readgivesmalformed, and a request's body is refused as any body that isn't JSON is, whether the object is anObject's, anInput'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_jwtrefuses a token whose claims do the same. A set's repeated item stays aduplicateproblem, since it is a value, not the document's shape. - The reader is Jadpo's own: RFC 8259's grammar as
serde_jsonreads 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 aCalendarDategoes is the date,5wherescale: 2goes is5.00,20%is the fraction0.20,2.5kmits metres, andCurrency.gbpthe codeGBP. 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'svisible:andwhen:, a transition'sset:, a create'sinitial:, 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 boundvisible:still proves a partial index's predicate.jadpo sqlshows 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.sqlruns 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'sactive: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. Sostandard_conforming_stringscan't change its meaning, and it stays on one line: a migration's sections are read line by line, so a\rwould 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'sTimeZonecan't change it; - a number is written only when it is digits, a sign and a point.
- text is quoted, with each
- 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 itina list of literals, compiles; it isn'tquery.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.