В JavaScript объект нередко используется как словарь: ключи
формируются динамически, а значения имеют общий тип. Для описания таких
структур в Zod используются z.record() и связанные
механизмы работы с ключами объектов.
Подобные схемы особенно полезны при:
z.record()Базовый способ описания словаря — функция
z.record().
import { z } from "zod";
const ScoresSchema = z.record(z.number());
Схема означает:
Проверка:
ScoresSchema.parse({
alice: 100,
bob: 95,
});
Ошибка:
ScoresSchema.parse({
alice: 100,
bob: "95",
});
Результат:
Expected number, received string
В JavaScript ключи обычного объекта автоматически приводятся к строке.
const obj = {
1: "one",
};
console.log(Object.keys(obj));
Результат:
["1"]
Поэтому z.record() по умолчанию всегда работает со
строковыми ключами.
Наиболее распространённый вариант:
const StringMap = z.record(z.string());
Пример:
StringMap.parse({
title: "Zod",
version: "4",
});
const TagsSchema = z.record(z.array(z.string()));
Данные:
{
article1: ["javascript", "zod"],
article2: ["typescript"],
}
z.record() часто комбинируется с
z.object().
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
const UsersMapSchema = z.record(UserSchema);
Использование:
UsersMapSchema.parse({
user1: {
id: 1,
name: "Alice",
},
user2: {
id: 2,
name: "Bob",
},
});
z.infertype UsersMap = z.infer<typeof UsersMapSchema>;
Тип:
type UsersMap = Record<
string,
{
id: number;
name: string;
}
>;
const DictionarySchema = z.record(
z.string().min(3)
);
const PricesSchema = z.record(
z.number().positive()
);
const CacheSchema = z.record(
z.string().nullable()
);
Пример:
{
item1: "value",
item2: null,
}
z.record() может принимать схему ключа первым
аргументом.
const SettingsSchema = z.record(
z.enum(["theme", "language"]),
z.string()
);
Разрешены только:
themelanguageПроверка:
SettingsSchema.parse({
theme: "dark",
language: "ru",
});
Ошибка:
SettingsSchema.parse({
theme: "dark",
locale: "ru",
});
z.literalconst FixedSchema = z.record(
z.literal("token"),
z.string()
);
Допустимо:
{
token: "abc123"
}
Недопустимо:
{
session: "abc123"
}
const Schema = z.record(
z.union([
z.literal("en"),
z.literal("ru"),
z.literal("de"),
]),
z.string()
);
type UserMap = Record<string, User>;
const UserMapSchema = z.record(UserSchema);
Комбинация:
type UserMap = z.infer<typeof UserMapSchema>;
z.object()Используется при фиксированной структуре.
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
z.record()Используется при динамических ключах.
const ScoresSchema = z.record(z.number());
Часто объект содержит как фиксированные поля, так и динамические.
const ConfigSchema = z.object({
appName: z.string(),
env: z.string(),
variables: z.record(z.string()),
});
Пример:
{
appName: "MyApp",
env: "production",
variables: {
API_URL: "https://api.test.com",
TOKEN: "secret",
},
}
const MatrixSchema = z.record(
z.record(z.number())
);
Данные:
{
row1: {
col1: 1,
col2: 2,
},
row2: {
col1: 3,
col2: 4,
},
}
const MixedSchema = z.record(
z.union([
z.string(),
z.number(),
z.boolean(),
])
);
.optional()const OptionalValuesSchema = z.record(
z.string().optional()
);
Допустимо:
{
a: "hello",
b: undefined,
}
.default()const SettingsSchema = z.record(
z.string()
).default({});
Пример:
SettingsSchema.parse(undefined);
Результат:
{}
.catchall()Иногда требуется фиксированный объект с дополнительными динамическими полями.
Для этого используется .catchall().
const ApiResponseSchema = z.object({
success: z.boolean(),
})
.catchall(z.string());
Допустимо:
{
success: true,
requestId: "abc",
traceId: "xyz",
}
Все дополнительные поля должны быть строками.
record и catchallrecordВесь объект состоит из динамических ключей.
z.record(z.string())
catchallЕсть фиксированные поля плюс дополнительные.
z.object({
id: z.number(),
}).catchall(z.string())
.strict()const UserSchema = z.object({
name: z.string(),
}).strict();
Дополнительные поля запрещены.
.passthrough()const UserSchema = z.object({
name: z.string(),
}).passthrough();
Дополнительные поля разрешены без проверки.
.catchall()const UserSchema = z.object({
name: z.string(),
}).catchall(z.number());
Дополнительные поля разрешены, но должны быть числами.
Прямой поддержки regex-ключей в record нет, однако это
можно реализовать через .superRefine().
const EnvSchema = z.record(z.string())
.superRefine((obj, ctx) => {
for (const key of Object.keys(obj)) {
if (!/^APP_/.test(key)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid key: ${key}`,
});
}
}
});
Допустимо:
{
APP_URL: "http://localhost",
APP_TOKEN: "secret",
}
Ошибка:
{
URL: "http://localhost"
}
const NumberRecordSchema = z.record(z.string())
.transform((obj) => {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k,
Number(v),
])
);
});
Пример:
NumberRecordSchema.parse({
a: "1",
b: "2",
});
Результат:
{
a: 1,
b: 2,
}
const UserIdsSchema = z.record(
z.string()
).superRefine(async (obj, ctx) => {
for (const [key, value] of Object.entries(obj)) {
const exists = await checkUser(value);
if (!exists) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Unknown user: ${value}`,
path: [key],
});
}
}
});
Использование:
await UserIdsSchema.parseAsync(data);
Иногда нужен частично заполненный словарь.
const LocaleSchema = z.record(
z.enum(["ru", "en", "de"]),
z.string()
);
Можно передавать:
{
ru: "Привет"
}
Все ключи не обязательны.
const ReadonlySchema = z.record(
z.string()
).readonly();
Тип:
Readonly<Record<string, string>>
Пример:
const Schema = z.record(z.number());
Schema.safeParse({
a: 1,
b: "2",
});
Результат:
{
success: false,
error: ZodError
}
Детали:
[
{
code: "invalid_type",
expected: "number",
received: "string",
path: ["b"],
message: "Expected number, received string"
}
]
const TranslationSchema = z.record(
z.string()
);
const EnvSchema = z.record(
z.string()
);
const CacheSchema = z.record(
z.any()
);
const ProductSchema = z.object({
id: z.number(),
title: z.string(),
});
const ProductsSchema = z.record(
ProductSchema
);
Многие API возвращают данные именно в формате словаря.
{
"users": {
"1": {
"name": "Alice"
},
"2": {
"name": "Bob"
}
}
}
Схема:
const ApiSchema = z.object({
users: z.record(
z.object({
name: z.string(),
})
),
});
z.record() валидирует:
При работе с большими объектами это может быть затратной операцией.
Особенно дорого обходятся:
superRefine;object вместо recordНеправильно:
z.object({})
для динамических ключей.
Правильно:
z.record(z.string())
z.any()z.record(z.any())
убирает преимущества типизации и валидации.
safeParseschema.parse(data)
генерирует исключение.
Для безопасной обработки лучше:
schema.safeParse(data)
record для словарейЕсли ключи заранее неизвестны:
z.record(...)
z.record(
z.enum(["ru", "en"]),
z.string()
)
anyПредпочтительно:
z.unknown()
или конкретные схемы.
objectФиксированные поля:
z.object(...)
Динамические:
z.record(...)
| Конструкция | Назначение |
|---|---|
z.record(valueSchema) |
словарь значений |
z.record(keySchema, valueSchema) |
словарь с ограничением ключей |
.catchall() |
дополнительные поля объекта |
.strict() |
запрет лишних полей |
.passthrough() |
пропуск дополнительных полей |
.readonly() |
readonly-словарь |
.transform() |
преобразование данных |
.superRefine() |
сложная пользовательская проверка |