# Decoding

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:

```jadpo
// packages/acme_couriers/v1/types/parcel.jadpo
public 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.jadpo
public 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](standard-library.md)). 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:`:

```jadpo
/// A booked collection.
public type Booking = Object {
    id: Text
    collect_at: Instant {
        wire_name: "collectAt"
    }
    part_of_day: SlotWindow {
        wire_name: "partOfDay"
    }
}
```

```jadpo
/// When in the day a collection comes.
public type SlotWindow = Enum {
    morning
    after_noon: {
        wire_name: "after-noon"
    }
}
```

```jadpo
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:

```jadpo
/// 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
    }
}
```

```jadpo
/// 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:`:

```jadpo
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
    }
}
```

```jadpo
/// 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:

```jadpo
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:

```jadpo
/// 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"
        }
    }
}
```

```jadpo
public resource charges {
    get: {
        method: HttpMethod.get
        path: "/v1/charges/{id}"
        input: ChargeLookup
        output: Charge
        changes: false
        source: PackageSource.written
    }
}
```

```jadpo
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.
