# 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](http.md), 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](data.md), 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](data.md) and [Reads and writes](reads-and-writes.md), this page |
| Caches and Redis entities | The contract's JSON, as [Caches](caches.md) holds the API's JSON there | [Data](data.md) |
| 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](acceptances.md), and a payload that no longer reads is parked, never dropped | [Jobs and events](jobs-and-events.md) |
| 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](data-formats-json-csv-tsv-and-json-lines.md) |
| JSON Lines rows, `json.read` and `json.write` | The contract's JSON | This page |
| Logs and traces | Values as [Logging](logging.md) and [Personal data](personal-data.md) 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](personal-data.md) |
| Settings | `text.parse`'s canonical text, with the source spellings a setting also takes (`5m`) | [Settings](settings-config-jadpo.md) |
| Translation placeholders and email | Shown to people by `text.format` (`PresentationText`), never read back | [Configuration and time](configuration-and-time.md) and [Built-in services](built-in-services.md) |
| Outbound provider calls and webhooks | The contract's JSON for a field without `wire:`; `wire:` only names another encoding a provider uses | [Integrations](external-services.md) |
| Records and the manifest | Types and defaults as source writes them; no runtime values | [Records and manifest](records-and-manifest.md) |
| Test fixtures and seeds | Written through the actions they call, so through storage | [Data](data.md) and [Testing](testing.md) |
| Files | Bytes; the format namespaces above turn them into values | [Files](files.md) |
| 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](debugging-and-review.md) |
| The interpreter and compiled code, native and wasm32 | One runtime crate, so one codec for each shape | [The compiler and runtime](https://github.com/jadpo-dev/jadpo/blob/main/docs/compiler-runtime.md) |

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

## 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](time-and-calendar-additions.md)).
- **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](structured-values.md)
  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](http.md) and
  [Time types](time-types.md) 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](one-input.md#the-query-string),
  [Paths and placeholders](paths-and-placeholders.md)). 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](settings-config-jadpo.md#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.
