Defjs

Define the input and response contracts your SDK shares with its consumers, with types and runtime validation.

Struct

Structs express the data contract your SDK promises: caller input, service responses, declared errors, and realtime messages. Reuse them across exported operations so consumers get consistent TypeScript types and runtime validation, independently of the client they configure.

You get TypeScript types via Infer, and runtime checks via struct.parse(...) — no throw, error-first tuple.

Basic Setup

import { defineRequest, struct, type Infer } from "@defjs/core";

const User = struct.object({
  id: struct.number(),
  name: struct.string(),
  active: struct.boolean(),
});

type User = Infer<typeof User>;

const createUser = defineRequest({
  method: "POST",
  path: "/users",
  input: struct.object({
    name: struct.string(),
    email: struct.string(),
  }),
  builder: (input) => ({ body: input }),
  output: User,
});

function readUser() {
  const [parseError, user] = struct.parse(User, {
    id: 7,
    name: "Ada",
    active: true,
  });
  if (parseError) {
    console.error(parseError.message);
    return;
  }
  console.log(user.name);
}

readUser();
void createUser;

Parsed output keeps only declared fields. Missing required fields, wrong primitives, bad nested values, wrong tuple length, or disallowed null → StructError, no partial value. Structs are immutable; .optional() and friends return a new Struct.

Required, optional, null

Presence and nullability are separate:

DeclarationMissing / undefinednullValid value
struct.string()RejectRejectAccept string
struct.string().optional()Accept; omit absent object fieldRejectAccept string
struct.string().null()RejectAcceptAccept string
struct.string().nullish()Accept; omit absent object fieldAcceptAccept string
struct.null()RejectAcceptReject other values
import { struct } from "@defjs/core";

const Profile = struct.object({
  name: struct.string(),
  nickname: struct.string().optional(),
  biography: struct.string().null(),
  note: struct.string().nullish(),
});

function showProfile() {
  const [error, profile] = struct.parse(Profile, {
    name: "Ada",
    biography: null,
    note: undefined,
  });
  if (error) {
    console.error(error.message);
    return;
  }
  console.log(profile.name, profile.nickname, profile.biography, profile.note);
}

showProfile();

struct.any() and struct.unknown() reject null and undefined too. Chain .null(), .optional(), or .nullish() to accept them.

At the root, optional can be undefined. Inside an object, omitted optional/nullish fields stay absent. A nested object field follows the same rule: mark it .optional() when the caller may omit it. A required object field stays required even when every field inside it is optional.

Request placement

Path, query, headers, and body are fields the builder returns, not a request struct. A declared input always requires an explicit builder. input only defines the parameters; the builder alone decides where each value goes. Only definitions without input may omit builder. struct.urlencoded(shape) and struct.formData(shape) are object structs with a codec tag. The tag runs only when that value is the body root: keys use aliases, and arrays repeat the field name.

import { defineRequest, struct, type Client } from "@defjs/core";

const createUser = defineRequest({
  method: "POST",
  path: "/organizations/:organizationId/users",
  input: struct.object({
    organizationId: struct.string(),
    notify: struct.boolean().optional(),
    requestId: struct.string(),
    user: struct.object({
      displayName: struct.string().alias("display_name"),
    }),
  }),
  builder: (input) => ({
    path: { organizationId: input.organizationId },
    query: { notify: input.notify },
    headers: { "x-request-id": input.requestId },
    body: input.user,
  }),
});

async function submit(client: Client) {
  const [error] = await createUser(client, {
    organizationId: "acme",
    notify: true,
    requestId: "request-42",
    user: { displayName: "Ada" },
  });
  if (error) return;
}

void submit;
Body rootSent asDefault content type
typed object, array, finite number, boolean, nullJSON textapplication/json
string struct or a raw stringtexttext/plain;charset=UTF-8
struct.urlencoded(shape) at the rootURLSearchParamsapplication/x-www-form-urlencoded;charset=UTF-8
struct.formData(shape) at the rootFormDataplatform multipart boundary
Blob / Filethe blobblob type or application/octet-stream
ArrayBuffer / view / streambytesapplication/octet-stream

NaN and ±Infinity, including inside an array or object, fail with REQ_BUILD_FAILED.

struct.file() is a value struct for a form field and for a raw file body. WebSocket builders cannot return headers or body. HTTP and SSE can. GET and HEAD cannot.

Aliases

.alias(...) separates logical names from wire names. struct.parse(...) uses logical keys unless you pass { aliases: true }. JSON and flat request codecs encode aliases; JSON response decoding maps wire keys back to logical fields.

import { struct } from "@defjs/core";

const User = struct.object({
  displayName: struct.string().alias("display_name"),
});

function readDisplayName() {
  const [parseError, user] = struct.parse(User, { displayName: "Ada" });
  if (parseError) {
    console.error(parseError.message);
    return;
  }
  console.log(user.displayName);
}

readDisplayName();

const [wireError] = struct.parse(User, { display_name: "Ada" });
// Expected failure: public parse reads logical keys, not wire keys.
if (!wireError) throw new Error("struct.parse must read logical keys");
console.log(wireError.issues[0]?.path);
BoundaryField
struct.parse(User, ...) and operation inputLogical displayName
Typed object returned from the builderWire display_name
JSON / SSE / WebSocket decodingWire → logical displayName
Anonymous object written in the builderThe key you wrote

The builder itself reads logical names (input.user.displayName); the wire name appears only when the serializer encodes the typed object the builder returns.

Aliases work on nested fields, arrays, objects, unions, and discriminators. Keep logical names in app code; put the external name on the struct once. Spreading a typed object keeps that encoding. Setting both the logical field and its wire key in the same object is REQ_BUILD_FAILED. JSON.stringify of a value yourself leaves this path: the serializer sees logical names.

The guide is chosen at every node, not only at the root. A new object under an anonymous parent keeps the keys you wrote. The same object under a typed parent uses that field's struct.

body: { row: { id: '42' } }              // Anonymous root: emits id
body: { ...input, row: { id: '42' } }    // Typed root, row field declares alias: emits wire_id

When a union guide matches more than one branch, each branch is encoded and compared. Identical wire output is kept; a difference is REQ_BUILD_FAILED.

A mixed intersection that contains a non-object part is tagged, when parsed, with the conjunction of the tags from the parts that matched; a newly written mixed-intersection value that was not parsed is an anonymous object.

Parse failures

struct.parse(...) returns [null, value] or [StructError, undefined]. StructError extends Error and exposes issues, plus format(), flatten(), and prettify().

import { struct, StructError } from "@defjs/core";

const User = struct.object({ id: struct.number(), name: struct.string() });
const [error, value] = struct.parse(User, { id: "not-a-number" });

if (error) {
  console.log(error instanceof StructError);
  console.log(error.issues[0]?.code, error.issues[0]?.path);
  console.log(error.flatten().fieldErrors);
  console.log(error.format(), error.prettify());
}
void value;

A StructIssue has code, expected, message, path, and received. code is custom, invalid_enum, invalid_intersection, invalid_literal, invalid_type, invalid_union, or missing_key. Default messages name the received kind, never the value: Expected number at id, received string. Issue paths are structural and retain record keys: struct.record(...) can produce Expected number at alice@example.com, received string, so keys may still be sensitive in default messages, prettify(), and String(fault). message and prettify() therefore omit received payloads; received keeps the original value and needs redaction before logging or returning. struct.parse(..., { errorMap }) rewrites messages for that call only, including bigint/date conversion failures in a struct from another Core copy. Any returned string, including '', replaces the default; only undefined keeps it. Custom text is used verbatim and may expose values if the map includes them.

Struct validation is structural only. No public range, format, refinement, auth, or state-transition rules. Do those checks before you call an operation.

Reference

Public constructors on @defjs/core (internals are not facade APIs):

import { struct } from "@defjs/core";

const Any = struct.any();
const ArrayOfStrings = struct.array(struct.string());
const Bytes = struct.arrayBuffer();
const BigIntValue = struct.bigint();
const BlobValue = struct.blob();
const BooleanValue = struct.boolean();
const DateValue = struct.date();
const Discriminated = struct.discriminatedUnion("kind", [
  struct.object({ kind: struct.literal("created"), id: struct.number() }),
  struct.object({ kind: struct.literal("deleted"), id: struct.number() }),
]);
const Status = struct.enum(["draft", "published"]);
const FileValue = struct.file();
const Form = struct.formData({ file: struct.file() });
const Combined = struct.intersection(
  struct.object({ id: struct.number() }),
  struct.object({ name: struct.string() }),
);
const Literal = struct.literal("ready");
const NullValue = struct.null();
const NumberValue = struct.number();
const ObjectValue = struct.object({ id: struct.number() });
const Union = struct.or(struct.string(), struct.number());
const RecordValue = struct.record(struct.number());
const StringValue = struct.string();
const Tuple = struct.tuple([struct.string(), struct.number()]);
const Unknown = struct.unknown();
const FormUrlEncoded = struct.urlencoded({ name: struct.string() });

void [
  Any,
  ArrayOfStrings,
  Bytes,
  BigIntValue,
  BlobValue,
  BooleanValue,
  DateValue,
  Discriminated,
  Status,
  FileValue,
  Form,
  Combined,
];
void [
  Literal,
  NullValue,
  NumberValue,
  ObjectValue,
  Union,
  RecordValue,
  StringValue,
  Tuple,
  Unknown,
  FormUrlEncoded,
];
ConstructorInputInferred output
struct.number()Number other than NaNnumber, including ±Infinity
struct.date()Date, number, or date stringValid Date
struct.bigint()bigint or string accepted by BigInt(...)bigint
struct.enum(...)Declared string or number memberThat literal union
struct.discriminatedUnion(...)Object with required literal discriminatorSelected object branch
struct.or(...)First matching branch; encoding checks ambiguityUnion of branch outputs
struct.intersection(...)Values accepted by every memberIntersection of outputs
struct.record(value)Plain object whose values match valueRecord of parsed values
struct.tuple(items)Array of exactly the declared lengthFixed-length tuple

Every Struct supports .alias(name), .optional(), .null(), and .nullish(). struct.discriminatedUnion needs object options with a required literal discriminator and rejects duplicates.

Import struct, Infer, Struct, StructError, and related public types from @defjs/core. Use struct.parse(...) as the parser. Don’t import createObjectStruct, definition symbols, codec internals, or packages/core/src.

Facade non-promises:

  • Object/record outputs use a null prototype — don’t assume Object.prototype methods.
  • Unknown object keys are dropped.
  • struct.number() rejects NaN, accepts infinities.
  • struct.or(...) tries branches in order; rejects ambiguous encodings when branches disagree.
  • struct.intersection(...) parses members in declaration order.
  • An intersection deep-merges member outputs (Object.is-equal values and equal-time dates stay, plain objects and equal-length arrays merge recursively, any other clash is invalid_intersection at that path), unknown/any contribute nothing, and one logical key may map to several wire keys. Only unequal values written to the same wire key conflict.
  • A Struct validates a boundary; it doesn’t cache, authorize, or own a transport resource.
Edit this page on GitHub
Released under the MIT License. · Copyright © 2026 MunMunMiao

On this page