k.issue
Emit a validation issue with a machine-readable code, checked against your declared issueCodes.
Emit a single Zod validation issue carrying a custom machine-readable code. The code is surfaced verbatim in the errors[].code field of kizuna's ValidationErrorSchema.
The code is checked against the issueCodes you declared on new Kizuna(), so a typo is a compile error rather than a silently accepted string.
Why it exists
Zod's runtime accepts any string as an issue code, but its types restrict ctx.addIssue() to the built-in issue union. A custom code therefore fails to type-check without an awkward cast at every call site:
// Without the helper, TS2322 unless you cast.
ctx.addIssue({ code: 'invalid_phone_number', message: 'Invalid phone number', input: value }
as unknown as Parameters<typeof ctx.addIssue>[0]);k.issue owns that cast internally so call sites stay clean.
Parameters
k.issue<Input>(
ctx: z.core.$RefinementCtx<Input>,
issue: { code: string; message: string; input: Input },
): void| Parameter | Type | Description |
|---|---|---|
ctx | z.core.$RefinementCtx<Input> | The refinement context from .superRefine(). |
issue.code | ValidationIssueCode | Custom machine-readable code surfaced in errors[].code. |
issue.message | string | Human-readable description of the failure. |
issue.input | Input | The value that failed validation. |
Example
import { z } from 'zod';
import { isValidPhoneNumber } from 'libphonenumber-js';
const CreateContactSchema = z.object({
phone: z.string().superRefine((value, ctx) => {
if (isValidPhoneNumber(value)) return;
k.issue(ctx, {
code: 'invalid_phone_number',
message: 'Invalid phone number',
input: value,
});
}),
});A request whose body fails this refinement returns 400 with the custom code in the response:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Request validation failed",
"errors": [
{
"code": "invalid_phone_number",
"path": ["phone"],
"message": "Invalid phone number"
}
]
}Typed codes on the client
By default errors[].code is typed as ValidationIssueCode, so the built-in Zod codes are suggested in autocomplete and any custom string is accepted, but your invalid_phone_number is not suggested (it lives only inside the refinement callback, where TypeScript can't see it).
To make the client suggest your custom codes, declare them on new Kizuna()'s validation.issueCodes for that API and pass the contract to new KizunaClient():
// k.ts
import { Kizuna } from '@ts-kizuna/core';
import { tags } from './tags';
export const k = new Kizuna({
tags,
validation: {
issueCodes: ['invalid_phone_number'],
},
});// api-client.ts
import { KizunaClient } from '@ts-kizuna/fetch';
import { contract } from './contract';
export const apiClient = new KizunaClient(contract, {
baseUrl: 'http://localhost:3000',
});Now errors[].code on a 400 response is widened to ValidationIssueCode | 'invalid_phone_number', so invalid_phone_number shows up in autocomplete when you read or compare it:
const result = await apiClient.contacts.createContact({ body: { phone: 'nope' } });
if (result.status === 400 && isValidationError(result.body)) {
for (const issue of result.body.errors) {
if (issue.code === 'invalid_phone_number') {
// ^ autocompleted, type-checked
}
}
}Each API has its own new Kizuna() call, so codes stay scoped per API rather than leaking across them.
See isValidationError for how clients consume these codes.