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: External services

External services

Decoding

On this page

Providers often send values in odd forms: a date as Unix seconds, a number as text, money as a whole number of pence. Decoding turns these into proper typed values, so your code only ever sees an Instant, a Decimal or Money. The package declares how each value is written on the wire, and the runtime decodes it.

Decoding is one of two kinds of conversion. To tell which one you need, ask: is it needed to make the value mean what the provider's documentation says it is?

  • Yes: decoding. Dates as Unix seconds, numbers as text, lists as "a,b,c", an envelope around the real object, an error inside a 200, a columnar table that is really a list of rows, or Stripe's amount: 1250, currency: "gbp", which is really money. The package declares it and the runtime does it.
  • No: translation. Turning a correct value into the shape one app prefers. Your app's action does it, with an explicit call.

Decoding is declared on fields and on operations:

example
// packages/acme_couriers/v1/types/parcel.jadpopublic type Parcel = Object {    collect_at: Instant {        wire: Wire.unix_seconds    }    weight: Decimal {        scale: 2        wire: Wire.text    }    price: Money {        wire: Wire.minor_units        currency: Currency.gbp    }    slot: SlotWindow {        decode: slots.parse_window        encode: slots.format_window    }}// packages/acme_couriers/v1/resources/tracking.jadpopublic resource tracking {    get: {        method: HttpMethod.get        path: "/v1/parcels/{id}/tracking"        input: TrackingLookup        wire_output: TrackingTable        output: List<TrackingEvent>        decode: tables.to_events        output_at: "data"        failed_when: {            path: "ok"            equals: false        }        changes: false        source: PackageSource.written        docs: "https://docs.acme-couriers.example/api#tracking"    }}
Written On Means
wire: Wire.x A field One of the built-in encodings: Unix seconds and milliseconds, ISO dates, numbers as text, joined lists, base64, JSON inside a string, money in minor units. A fixed list extended by Jadpo releases
decode: and encode: A field A pure function pair from the package's own libraries/, for anything else; checked to round-trip at publish
wire_output: and decode: An operation The response as sent, and a pure function turning it into output:; run over every recorded response at publish
output_at: An operation The path of the real object inside an envelope
failed_when: An operation A 200 answer that is really a failure, by a path and the value that marks it (ok is false); the failure's code is then read from error_code:
  • A field without wire: is written as Jadpo's contract writes it. The contract is the one JSON form each standard type has at every boundary (Standard library). A standard type in a provider's request, response or webhook is encoded that way, by the same codec a route's request body goes through. So an amount is {"amount": 1250, "currency": "GBP"}, and a quantity keeps its unit. Write wire: only where the provider uses another encoding.
  • Byte formats stay in the runtime: JSON, form-encoded, multipart, and XML later. Package functions reshape values, never bytes.
  • Decoding is lazy where it can be. A field decoder runs only when code reads the field. A whole-response decoder reads what it reads, so a provider adding fields it ignores breaks nothing.
  • A value that won't decode is a fault naming the operation and field, as a field that doesn't match its type is. Contract runs against the sandbox catch it before production does.
  • App tests script the clean type, never the wire shape. The package's own tests and recordings cover its decoders. Webhook payloads decode the same way.
  • The importer can't see most of this, so the agent adds it with the other corrections, and jadpo package record confirms it against a real response.
  • The compiler can't police the line between decoding and translation. The surface shows both the wire type and the output type, so the published documentation and the review make it visible.

Spellings: wire_name:

Write wire_name: on a provider package's field or enum variant only where the provider's spelling differs from the Jadpo name. The runtime then sends and reads that spelling. A field or variant gives its spelling in its own block. Here failed_when: also reads the failure's code from the provider's error_code::

example
/// A booked collection.public type Booking = Object {    id: Text    collect_at: Instant {        wire_name: "collectAt"    }    part_of_day: SlotWindow {        wire_name: "partOfDay"    }}
example
/// When in the day a collection comes.public type SlotWindow = Enum {    morning    after_noon: {        wire_name: "after-noon"    }}
example
public resource bookings {    create: {        method: HttpMethod.post        path: "/v1/bookings"        input: CreateBooking        output: Booking        output_at: "data"        failed_when: {            path: "ok"            equals: false        }        changes: true        idempotent: true        failures: [SlotTaken]        source: PackageSource.written    }}

So {"ok": true, "data": {"id": "b-1", "collectAt": "2026-10-20T09:00:00Z", "partOfDay": "after-noon"}} is read as a Booking with collect_at and part_of_day: SlotWindow.after_noon, and {"ok": false, "error": {"code": "slot_taken"}} as the operation's SlotTaken.

The built-in encodings: wire:

A field names one of these ten built-in encodings with wire::

wire: On a field of The provider sends
Wire.unix_seconds Instant A whole number of seconds since 1970: 1792400400
Wire.unix_milliseconds Instant A whole number of milliseconds since 1970
Wire.iso_8601 Instant ISO 8601 text with any offset from UTC, "2026-10-19T10:00:00+01:00"; written in UTC
Wire.day_month_year CalendarDate "19/10/2026"
Wire.text Int, Decimal The number as text: "3", "2.50"
Wire.number Int, Decimal A JSON number, every place written kept: 2.50
Wire.comma_separated A list of text, numbers or an enum's plain variants One text, joined by commas: "fragile,heavy"
Wire.base64 Text The Base64 of its UTF-8
Wire.json_text Anything Its JSON, inside a string
Wire.minor_units Money whose type fixes its currency: A whole number of the currency's minor units: 1250

A field's own functions: decode: and encode:

For anything the built-in encodings don't cover, a field names a pair of functions. decode: takes the value as the provider sends it and gives the field's type, failing when it can't. encode: turns the field's type back, without failing. Both live in the package's own libraries/, so they may be internal:

example
/// A parcel and where it is.public type Parcel = Object {    id: ParcelRef    status: Status    collect_at: CollectionTime {        wire: Wire.unix_seconds        wire_name: "collectAt"    }    weight: Weight {        wire: Wire.number    }    price: ParcelPrice {        wire: Wire.minor_units    }    slot: SlotWindow {        decode: courier_windows.parse_window        encode: courier_windows.format_window    }}
example
/// The courier's collection windows, which it writes as "AM" and "PM".library courier_windows {    /// The window the courier's code names.    function parse_window(code: WindowCode) fails WindowUnknown -> SlotWindow {        if (code == "AM") {            return SlotWindow.morning        }        if (code == "PM") {            return SlotWindow.afternoon        }        reject WindowUnknown    }    /// The courier's code for a window.    function format_window(pickup_window: SlotWindow) -> WindowCode {        if (pickup_window == SlotWindow.morning) {            return "AM"        }        return "PM"    }}

A whole answer: wire_output:

An operation's wire_output: is the whole answer as sent, read from inside output_at:. Its decode: turns it into output::

example
public resource tracking {    get: {        method: HttpMethod.get        path: "/v1/parcels/{id}/tracking"        input: ParcelLookup        wire_output: TrackingTable        output: List<TrackingEvent>        decode: tracking_sheets.to_events        output_at: "data"        changes: false        source: PackageSource.written    }}
example
/// The courier's tracking history, as a list of where a parcel was seen.library tracking_sheets {    /// Each sighting as an event.    function to_events(history_sheet: TrackingTable) -> List<TrackingEvent> {        return list.map(            history_sheet.sightings,            sighting -> TrackingEvent {                hub: sighting.hub            }        )    }}

An action that reads one field of an answer decodes that field alone. So {"id": "p-1", "status": "held", "collectAt": "soon", "slot": "NOON"} is read for its status, even though its time and its window would never decode:

example
use app { couriers }use shared/providers/acme_couriers/// Where a parcel is.action parcel_status(parcel: AcmeCouriers.ParcelRef) -> AcmeCouriers.Status {    var tracked_parcel: AcmeCouriers.Parcel = couriers.parcels.get(        AcmeCouriers.ParcelLookup {            id: parcel        }    )    return tracked_parcel.status}

Parts sent separately: wire_parts:

Some providers send a standard type's parts as separate members of the object that holds it. Stripe sends "amount": 1050, "currency": "gbp" beside a charge's other members. In Jadpo that is one field, whose wire_parts: names the member each part is read from and written to. The field has no member of its own on the provider's wire. Fields may share one part's member, as a charge's amounts share its currency:

example
/// A charge to a card.public type Charge = Object {    id: ChargeId    amount: Amount {        wire_parts: {            amount: "amount"            currency: "currency"        }    }    amount_refunded: Amount {        wire_parts: {            amount: "amount_refunded"            currency: "currency"        }    }    merchant: Point? {        wire_parts: {            latitude: "lat"            longitude: "lng"        }    }}
example
public resource charges {    get: {        method: HttpMethod.get        path: "/v1/charges/{id}"        input: ChargeLookup        output: Charge        changes: false        source: PackageSource.written    }}
example
use app { payments }use shared/providers/tidepay/// How much of a charge has been refunded.action refunded_so_far(charge: Tidepay.ChargeId) -> Tidepay.Amount {    var paid_charge: Tidepay.Charge = payments.charges.get(        Tidepay.ChargeLookup {            id: charge        }    )    return paid_charge.amount_refunded}

So {"id": "ch_1", "amount": 1050, "amount_refunded": 250, "currency": "gbp", "lat": 51.5, "lng": -0.125} is read as a Charge of £10.50, with £2.50 refunded, at a merchant at 51.5° N, 0.125° W. A Charge is sent back as those six members, the currency once. Two amounts in different currencies can't be sent, since they share one member, and the call stops as one that doesn't encode.

A Point is read from its latitude and longitude, which GeoJSON would send as a pair. Any other standard type made of named parts is read from those parts: Money's amount, in minor units, and currency; InstantRange's start and end. A field none of whose members is sent is none where it may be, and one with only some is a fault naming the missing member. Everywhere else, the field is written as the contract writes its type.

How decoding runs

  • Where they apply. On the provider's wire: an operation's input, its answer and its webhooks. Everywhere else, a json.write, a route's answer or an event's payload, the field is written as the contract writes its type, so the app's clients never see a provider's encoding.
  • Run at the call's site. A field's decode: and encode:, and an operation's decode:, run on the executor where the call is made, as any function the action calls would; one that fails, or gives no value, is a fault naming the operation and the field, never the value.
  • Only the fields read. The answer to a call held in a variable that code only reads fields of (tracked_parcel.status) is decoded for those fields alone; returning it, passing it on, or anything else uses it whole. Fields further in are decoded with the field that holds them. An answer decoded whole by an operation's decode: is read whole.

Markdown for agents: /docs/decoding.md.Built from Jadpo main at a707efb.