Skip to content

02Guides

Read and write models

readOnly and writeOnly properties: separate request and response models.

Many APIs describe one schema for both directions and mark the differences: readOnly properties are set by the server (an id, a createdAt), writeOnly ones are only sent (a password). A single TypeScript type for both is wrong both ways: requests would demand the id the server assigns, and responses would promise a password they never contain.

With types.readWrite: 'split' (the default), swaggerz generates two types where the specification says they differ:

import { createAccount, type Account, type AccountInput } from './generated/api';
const input: AccountInput = { name: 'Ada', password: 'correct horse battery staple' };
const created: Account = await createAccount({ body: input });
console.log(created.id); // set by the server; `password` is not part of the response type
  • The response type (Account) keeps readOnly properties, as readonly (see types.readonlyProperties), and leaves out writeOnly ones.
  • The request type (AccountInput, named with naming.inputSuffix) leaves out readOnly properties and keeps writeOnly ones.

A model gets an …Input variant only when it has readOnly or writeOnly properties, directly or through the models it contains, and something sends it in a request. Models without such properties, and models that only travel in one direction, are generated once, so most specifications see no extra types.

Webhook and callback payloads use the inverted directions: the API sends the payload, and your handler’s response goes back.

types.readWrite: 'merged' generates one type per schema, as most generators do. Use it when the specification marks properties inconsistently and the split types get in the way.

  • The setting applies to the whole client; a single model cannot opt out.
  • Validation schemas are split the same way (AccountInputSchema), so a request body is validated without its readOnly properties.

What's generated

read-write.yaml
openapi: 3.1.0
info:
title: Accounts
version: '1.0'
paths:
/accounts:
post:
tags: [accounts]
operationId: createAccount
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Account'
responses:
'201':
description: The new account.
content:
application/json:
schema:
$ref: '#/components/schemas/Account'
components:
schemas:
Account:
type: object
required: [id, name, password]
properties:
id:
type: string
readOnly: true
name:
type: string
password:
type: string
writeOnly: true

models/account.ts

/* eslint-disable */
// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
export type Account = {
readonly id: string;
name: string;
};

models/account-input.ts

/* eslint-disable */
// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
export type AccountInput = {
name: string;
password: string;
};

accounts/create-account.ts

/* eslint-disable */
// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
import { type RequestOptions, client } from '../client.js';
import type { AccountInput } from '../models/account-input.js';
import type { Account } from '../models/account.js';
import { type ApiError, type RequestError, isApiError } from '../runtime/errors.js';
import type { HttpStatus } from '../runtime/types.js';
/** Request body of {@link createAccount} (`application/json`). */
export type CreateAccountBody = AccountInput;
/** Parameters of {@link createAccount}. */
export type CreateAccountParams = {
/** Request body. */
body: CreateAccountBody;
};
/** Response data of {@link createAccount}. */
export type CreateAccountResponse = Account;
/** Body of an error response of {@link createAccount}. */
export type CreateAccountErrorBody = unknown;
/**
* The `ApiError`s {@link createAccount} rejects with: one per documented status, so checking `status` narrows `body`.
*
* The last member covers statuses the specification does not document.
*/
export type CreateAccountApiError = ApiError<unknown, HttpStatus<1 | 3 | 4 | 5>>;
/** Everything {@link createAccount} rejects with: an `ApiError` for an error status, or a `RequestError` when no usable response arrived. */
export type CreateAccountError = CreateAccountApiError | RequestError;
/**
* Calls `POST /accounts`.
*
* @remarks `POST /accounts` (operationId `createAccount`)
* @param params - Request body.
* @param options - Per-request options: `signal`, `timeout`, `headers`, `client`.
* @returns `201` The new account
* @throws {@link ApiError} When the server responds with a non-2xx status; narrow it with {@link isCreateAccountApiError}.
* @throws {@link RequestError} When no usable response is received (network failure, timeout or invalid body).
*/
export function createAccount(params: CreateAccountParams, options?: RequestOptions): Promise<CreateAccountResponse> {
return (options?.client ?? client).request<CreateAccountResponse>(
{
operationId: 'createAccount',
method: 'POST',
path: '/accounts',
body: params.body,
mediaType: 'application/json',
accept: 'application/json',
},
options,
);
}
/** Whether `error` is an `ApiError` rejected by {@link createAccount}. It narrows to {@link CreateAccountApiError}, so checking `status` afterwards narrows `body` too. */
export function isCreateAccountApiError(error: unknown): error is CreateAccountApiError {
return (
isApiError(error) &&
error.operationId === 'createAccount' &&
error.method === 'POST'
);
}
Generated by swaggerz 0.1.2, at build time.