Skip to content

GenericHttpRequest body constraint rejects codecs that decode to bigint #1116

@OttoAllmendinger

Description

@OttoAllmendinger

Problem

GenericHttpRequest defines its body as optional(Json):

// packages/io-ts-http/src/httpRequest.ts
export const GenericHttpRequest = optionalized({
  // ...
  body: optional(Json),
});

This produces body: t.UnionC<[t.Type<Json, Json, unknown>, t.UndefinedC]>, constraining both the decoded type (A) and encoded type (O) to Json.

Json is boolean | number | string | null | JsonArray | JsonRecord — it does not include bigint.

When a route intersects GenericHttpRequest with a body codec that uses BigIntFromString (from io-ts-types), the decoded type contains bigint, which is not assignable to Json. This causes a type error on the superagent-wrapper client side when calling .post(), because the parameter type is RequestType<Route> (i.e., t.TypeOf<request> — the decoded type).

BigIntFromString is t.Type<bigint, string, unknown> — it encodes bigint → string, which is valid Json. So the wire format is fine; only the decoded (in-memory) type violates the constraint.

Example

import { GenericHttpRequest, httpRoute } from '@api-ts/io-ts-http';
import { BigIntFromString } from 'io-ts-types';
import * as t from 'io-ts';

const MyRequest = t.type({ amount: BigIntFromString });

const route = httpRoute({
  path: '/transfer',
  method: 'POST',
  request: t.intersection([
    GenericHttpRequest,
    t.type({ body: MyRequest }),
  ]),
  response: { 200: t.unknown },
});

// On the superagent-wrapper client:
// client['transfer'].post({ query: {}, body: { amount: 100n } })
//                                               ^^^^
// TS2345: Type 'bigint' is not assignable to type 'Json'

Documentation discrepancy

The Docusaurus reference docs describe GenericHttpRequest as having body?: unknown, but the code constrains it to body?: Json.

Suggestion

Relax the body constraint to optional(t.unknown):

export const GenericHttpRequest = optionalized({
  params: t.record(t.string, t.string),
  query: t.record(t.string, t.union([t.string, t.array(t.string)])),
  headers: optional(t.record(t.string, t.string)),
  body: optional(t.unknown),
});

This matches the docs, and is safe because:

  • Each route already specifies its own body codec with the actual constraint
  • superagent-wrapper already handles bigint serialization at runtime (via a custom JSON.stringify replacer)
  • The Json constraint on the decoded type is overly restrictive for codecs like BigIntFromString that encode to valid JSON

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions